diff --git a/__tests__/pathResolver.test.ts b/__tests__/pathResolver.test.ts new file mode 100644 index 0000000..c835741 --- /dev/null +++ b/__tests__/pathResolver.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { existsSync } from 'fs' +import path from 'path' +import { resolveProjectPath, clearPathCache } from '../electron/pathResolver' + +// Mock fs.existsSync to control file system behavior in tests +vi.mock('fs', () => ({ + existsSync: vi.fn() +})) + +const mockExistsSync = vi.mocked(existsSync) + +describe('pathResolver', () => { + beforeEach(() => { + // Clear cache before each test + clearPathCache() + // Reset all mocks + vi.clearAllMocks() + }) + + describe('resolveProjectPath', () => { + it('should handle empty input', () => { + mockExistsSync.mockReturnValue(false) + const result = resolveProjectPath('') + expect(result).toBe('/') + }) + + it('should handle paths that are already in correct format', () => { + const result = resolveProjectPath('/home/user/project') + expect(result).toBe('/home/user/project') + }) + + describe('Unix path scenarios (/ separator)', () => { + it('should handle simple Unix paths with existing directories', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + return pathStr.includes('/home') || + pathStr.includes('/home/davidwei') || + pathStr.includes('/home/davidwei/bin') + }) + + const result = resolveProjectPath('-home-davidwei-bin', path.posix) + expect(result).toBe('/home/davidwei/bin') + }) + + it('should handle underscore detection in Unix paths', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Only the underscore version exists, not the slash version + return pathStr === '/home/davidwei/AndroidStudioProjects/happy_notes' || + pathStr === '/home/davidwei/AndroidStudioProjects' || + pathStr === '/home/davidwei' || + pathStr === '/home' || + pathStr === '/' + }) + + const result = resolveProjectPath('-home-davidwei-AndroidStudioProjects-happy-notes', path.posix) + expect(result).toBe('/home/davidwei/AndroidStudioProjects/happy_notes') + }) + + it('should fallback to dash separation when directories do not exist', () => { + mockExistsSync.mockReturnValue(false) + const result = resolveProjectPath('-home-someuser-unknown-project', path.posix) + expect(result).toBe('/home/someuser/unknown/project') + }) + }) + + describe('Windows path scenarios (\\ separator)', () => { + it('should handle basic Windows paths without file existence checking', () => { + mockExistsSync.mockReturnValue(false) + const result = resolveProjectPath('C--Users-David-Wei-bin', path.win32) + expect(result).toBe('C:\\Users\\David\\Wei\\bin') + }) + + it('should prefer David.Wei when that directory exists', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Match actual path format from algorithm (no C: prefix during processing) + return pathStr === '\\Users\\David.Wei' || + pathStr === '\\Users\\David.Wei\\bin' || + pathStr === '\\Users' || + pathStr === '\\' + }) + + const result = resolveProjectPath('C--Users-David-Wei-bin', path.win32) + expect(result).toBe('C:\\Users\\David.Wei\\bin') + }) + + it('should prefer David\\Wei when that directory structure exists', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Match actual path format: only David\Wei version exists + return pathStr === '\\Users\\David' || + pathStr === '\\Users\\David\\Wei' || + pathStr === '\\Users\\David\\Wei\\bin' || + pathStr === '\\Users' || + pathStr === '\\' + }) + + const result = resolveProjectPath('C--Users-David-Wei-bin', path.win32) + expect(result).toBe('C:\\Users\\David\\Wei\\bin') + }) + + it('should handle HappyNotes.Api when that directory exists', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Match actual path format: both David.Wei and HappyNotes.Api exist + return pathStr === '\\' || + pathStr === '\\Users' || + pathStr === '\\Users\\David.Wei' || + pathStr === '\\Users\\David.Wei\\RiderProjects' || + pathStr === '\\Users\\David.Wei\\RiderProjects\\HappyNotes.Api' + }) + + const result = resolveProjectPath('C--Users-David-Wei-RiderProjects-HappyNotes-Api', path.win32) + expect(result).toBe('C:\\Users\\David.Wei\\RiderProjects\\HappyNotes.Api') + }) + + it('should use cache for repeated calls', () => { + mockExistsSync.mockReturnValue(false) + + const result1 = resolveProjectPath('C--Users-test', path.win32) + const result2 = resolveProjectPath('C--Users-test', path.win32) + + expect(result1).toBe(result2) + expect(result1).toBe('C:\\Users\\test') + }) + + it('should preserve original hyphen directories when they actually exist', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Only the original David-Wei directory exists (not David.Wei or David\Wei) + return pathStr === '\\Users\\David-Wei' || + pathStr === '\\Users\\David-Wei\\bin' || + pathStr === '\\Users' || + pathStr === '\\' + }) + + const result = resolveProjectPath('C--Users-David-Wei-bin', path.win32) + expect(result).toBe('C:\\Users\\David-Wei\\bin') + }) + }) + + describe('Unix path edge cases', () => { + it('should preserve original hyphen directories in Unix paths when they exist', () => { + mockExistsSync.mockImplementation((path: string) => { + const pathStr = String(path) + // Only the original happy-notes directory exists (not happy_notes) + return pathStr === '/home/davidwei/AndroidStudioProjects/happy-notes' || + pathStr === '/home/davidwei/AndroidStudioProjects' || + pathStr === '/home/davidwei' || + pathStr === '/home' || + pathStr === '/' + }) + + const result = resolveProjectPath('-home-davidwei-AndroidStudioProjects-happy-notes', path.posix) + expect(result).toBe('/home/davidwei/AndroidStudioProjects/happy-notes') + }) + }) + }) + +}) \ No newline at end of file diff --git a/electron/pathResolver.ts b/electron/pathResolver.ts index 259625f..a4619be 100644 --- a/electron/pathResolver.ts +++ b/electron/pathResolver.ts @@ -1,5 +1,6 @@ import { existsSync } from 'fs' -import { join, resolve, sep } from 'path' +import path from 'path' +const { join, resolve, sep } = path // Map for caching const pathCache = new Map() @@ -8,13 +9,13 @@ const pathCache = new Map() * Restore Claude project folder name to actual file system path * Example: -Users-lullu-mainpy-claude-code-web → /Users/lullu/mainpy/claude-code-web * Windows: C--Users-username-project → C:\Users\username\project - * + * * Algorithm: - * - Windows: Extract drive letter and replace '--' with '\' + * - Windows: Extract drive letter and replace '--' with '\' * - Unix: Start by replacing the first '-' with path separator, then for each '-', * check if directory exists when replaced with path separator */ -export function resolveProjectPath(projectName: string): string { +export function resolveProjectPath(projectName: string, pathImpl: typeof path = path): string { // Check cache if (pathCache.has(projectName)) { return pathCache.get(projectName)! @@ -22,75 +23,75 @@ export function resolveProjectPath(projectName: string): string { // Empty string or only '-' case if (!projectName || projectName === '-') { - const result = resolve(sep) + const result = pathImpl.resolve(pathImpl.sep) pathCache.set(projectName, result) return result } // Check if this is a Windows project folder (starts with drive letter) const isWindowsProjectFolder = /^[A-Za-z]--/.test(projectName) - - // Unix project folders start with '-', Windows folders start with drive letter + + // Handle paths that are already in correct format (not encoded) if (!projectName.startsWith('-') && !isWindowsProjectFolder) { - // Already in path format pathCache.set(projectName, projectName) return projectName } - // Handle Windows project folders (e.g., "C--Users-username-project") + // Handle Windows project folders by converting them to Unix-like format for processing + let workingProjectName = projectName + let drivePrefix = '' + if (isWindowsProjectFolder) { - // Extract drive letter and remaining path const driveMatch = projectName.match(/^([A-Za-z])--(.+)$/) if (driveMatch) { const [, driveLetter, pathPart] = driveMatch - // Replace -- with :\ first, then replace remaining - with \ - // This may have false positives but gives more readable paths - let windowsPath = `${driveLetter.toUpperCase()}--${pathPart}` - windowsPath = windowsPath.replace(/--/, ':\\').replace(/-/g, '\\') - const result = windowsPath - pathCache.set(projectName, result) - return result + drivePrefix = `${driveLetter.toUpperCase()}:` + workingProjectName = `-${pathPart}` // Convert to Unix-like format for processing } } // Array to construct result path const parts: string[] = [] - - // Remove first '-' as it represents root (Unix paths only) - let remaining = projectName.substring(1) - + + // Remove first '-' as it represents root + let remaining = workingProjectName.substring(1) + // Handle empty directory names (starting with --) while (remaining.startsWith('-')) { // Consecutive '-' means empty directory parts.push('') remaining = remaining.substring(1) } - - // Path constructed so far - let currentPath = sep - + + // Path constructed so far - start with root path using pathImpl + let currentPath: string = pathImpl.sep + while (remaining.length > 0) { // Find next '-' position let nextDashIndex = remaining.indexOf('-') - + if (nextDashIndex === -1) { // If no more '-', treat the rest as one part parts.push(remaining) - currentPath = join(currentPath, remaining) + currentPath = pathImpl.join(currentPath, remaining) break } - + // Check path when '-' is replaced with path separator const possiblePart = remaining.substring(0, nextDashIndex) - const possiblePathAsSlash = join(currentPath, possiblePart) - + const possiblePathAsSlash = pathImpl.join(currentPath, possiblePart) + // Also check path when '-' is replaced with '_' - const possiblePathAsUnderscore = join(currentPath, possiblePart.replace(/-/g, '_')) - + const possiblePathAsUnderscore = pathImpl.join(currentPath, possiblePart.replace(/-/g, '_')) + + // NEW: Also check path when '-' is replaced with '.' + const possiblePathAsDot = pathImpl.join(currentPath, possiblePart.replace(/-/g, '.')) + // Resolve paths to handle Windows drive letters and normalize - const resolvedPathAsSlash = resolve(possiblePathAsSlash) - const resolvedPathAsUnderscore = resolve(possiblePathAsUnderscore) - + const resolvedPathAsSlash = pathImpl.resolve(possiblePathAsSlash) + const resolvedPathAsUnderscore = pathImpl.resolve(possiblePathAsUnderscore) + const resolvedPathAsDot = pathImpl.resolve(possiblePathAsDot) + if (existsSync(resolvedPathAsSlash)) { // If directory exists, separate with path separator parts.push(possiblePart) @@ -101,25 +102,33 @@ export function resolveProjectPath(projectName: string): string { parts.push(possiblePart.replace(/-/g, '_')) currentPath = possiblePathAsUnderscore remaining = remaining.substring(nextDashIndex + 1) + } else if (existsSync(resolvedPathAsDot)) { + // NEW: If exists when replaced with '.', process with '.' + parts.push(possiblePart.replace(/-/g, '.')) + currentPath = possiblePathAsDot + remaining = remaining.substring(nextDashIndex + 1) } else { // If directory doesn't exist, include up to next '-' as one part // Example: claude-code-web case let foundValid = false let searchIndex = nextDashIndex + 1 - + while (searchIndex < remaining.length) { const nextSearchIndex = remaining.indexOf('-', searchIndex) if (nextSearchIndex === -1) { // Search to the end const testPart = remaining - const testPath = join(currentPath, testPart) + const testPath = pathImpl.join(currentPath, testPart) const testPartWithUnderscore = testPart.replace(/-/g, '_') - const testPathWithUnderscore = join(currentPath, testPartWithUnderscore) - + const testPathWithUnderscore = pathImpl.join(currentPath, testPartWithUnderscore) + const testPartWithDot = testPart.replace(/-/g, '.') + const testPathWithDot = pathImpl.join(currentPath, testPartWithDot) + // Resolve paths for Windows compatibility - const resolvedTestPath = resolve(testPath) - const resolvedTestPathWithUnderscore = resolve(testPathWithUnderscore) - + const resolvedTestPath = pathImpl.resolve(testPath) + const resolvedTestPathWithUnderscore = pathImpl.resolve(testPathWithUnderscore) + const resolvedTestPathWithDot = pathImpl.resolve(testPathWithDot) + if (existsSync(resolvedTestPath)) { parts.push(testPart) currentPath = testPath @@ -130,22 +139,32 @@ export function resolveProjectPath(projectName: string): string { currentPath = testPathWithUnderscore remaining = '' foundValid = true + } else if (existsSync(resolvedTestPathWithDot)) { + parts.push(testPartWithDot) + currentPath = testPathWithDot + remaining = '' + foundValid = true } break } - + // Test including up to next '-' const testPart = remaining.substring(0, nextSearchIndex) - const testPath = join(currentPath, testPart) - + const testPath = pathImpl.join(currentPath, testPart) + // Also test path with '_' const testPartWithUnderscore = testPart.replace(/-/g, '_') - const testPathWithUnderscore = join(currentPath, testPartWithUnderscore) - - // Resolve paths for Windows compatibility - const resolvedTestPath = resolve(testPath) - const resolvedTestPathWithUnderscore = resolve(testPathWithUnderscore) - + const testPathWithUnderscore = pathImpl.join(currentPath, testPartWithUnderscore) + + // Also test path with '.' + const testPartWithDot = testPart.replace(/-/g, '.') + const testPathWithDot = pathImpl.join(currentPath, testPartWithDot) + + // Resolve paths for Windows compatibility + const resolvedTestPath = pathImpl.resolve(testPath) + const resolvedTestPathWithUnderscore = pathImpl.resolve(testPathWithUnderscore) + const resolvedTestPathWithDot = pathImpl.resolve(testPathWithDot) + if (existsSync(resolvedTestPath)) { parts.push(testPart) currentPath = testPath @@ -158,26 +177,38 @@ export function resolveProjectPath(projectName: string): string { remaining = remaining.substring(nextSearchIndex + 1) foundValid = true break + } else if (existsSync(resolvedTestPathWithDot)) { + parts.push(testPartWithDot) + currentPath = testPathWithDot + remaining = remaining.substring(nextSearchIndex + 1) + foundValid = true + break } - + searchIndex = nextSearchIndex + 1 } - + if (!foundValid) { - // If nothing matches, treat the whole as one - parts.push(remaining) - currentPath = join(currentPath, remaining) - break + // If directory doesn't exist, fall back to treating first part as directory + parts.push(possiblePart) + currentPath = pathImpl.join(currentPath, possiblePart) + remaining = remaining.substring(nextDashIndex + 1) } } } - + // Use the currentPath as the result since it's already built correctly - const result = currentPath - + let result = currentPath + + // For Windows paths, add drive prefix + if (drivePrefix) { + // Add drive prefix (path already uses correct separator) + result = drivePrefix + result + } + // Save to cache pathCache.set(projectName, result) - + return result } @@ -200,4 +231,5 @@ export function getPathCacheSize(): number { */ export function getCachedPath(projectName: string): string | undefined { return pathCache.get(projectName) -} \ No newline at end of file +} + diff --git a/package-lock.json b/package-lock.json index 8f376c2..ea64496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-viewer", - "version": "1.0.3", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-viewer", - "version": "1.0.3", + "version": "1.0.5", "license": "MIT", "dependencies": { "@electron-toolkit/preload": "^3.0.2", @@ -26,6 +26,9 @@ "uuid": "^11.0.4", "zustand": "^5.0.5" }, + "bin": { + "ccviewer": "cli/claude-viewer-cli.js" + }, "devDependencies": { "@electron/notarize": "^2.3.0", "@tailwindcss/postcss": "^4.1.7", @@ -44,7 +47,8 @@ "postcss": "^8.5.3", "ts-node": "^10.9.2", "typescript": "^5.8.3", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^3.2.4" } }, "node_modules/@alloc/quick-lru": { @@ -2467,6 +2471,16 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -2539,6 +2553,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2720,6 +2741,121 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3005,6 +3141,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3464,6 +3610,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3534,6 +3697,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4083,6 +4256,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4710,6 +4893,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4820,12 +5010,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -6303,6 +6513,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7867,6 +8084,23 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -8738,6 +8972,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -8902,6 +9143,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -8912,6 +9160,13 @@ "node": ">= 6" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8994,6 +9249,26 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", @@ -9220,10 +9495,24 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9237,6 +9526,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -9719,6 +10038,102 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -9745,6 +10160,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 07e2bfa..0b48dde 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "electron-vite build", "preview": "electron-vite preview", "start": "electron-vite preview", + "test": "vitest", "dist": "npm run build && electron-builder", "dist:mac": "npm run build && electron-builder --mac", "dist:win": "npm run build && electron-builder --win", @@ -41,7 +42,8 @@ "postcss": "^8.5.3", "ts-node": "^10.9.2", "typescript": "^5.8.3", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^3.2.4" }, "dependencies": { "@electron-toolkit/preload": "^3.0.2", diff --git a/src/components/Tools/ToolPreview.tsx b/src/components/Tools/ToolPreview.tsx index 4fd0124..c8a327c 100644 --- a/src/components/Tools/ToolPreview.tsx +++ b/src/components/Tools/ToolPreview.tsx @@ -1,4 +1,5 @@ -import React from 'react' +import React, { useState, useEffect } from 'react' +import { Copy, Check } from 'lucide-react' interface ToolPreviewProps { toolName: string @@ -21,6 +22,69 @@ export const ToolPreview: React.FC = ({ onMouseEnter, onMouseLeave }) => { + const [copiedParams, setCopiedParams] = useState(false) + const [copiedResult, setCopiedResult] = useState(false) + const [isSelecting, setIsSelecting] = useState(false) + + const handleCopyParameters = async () => { + if (!parameters) return + + try { + await navigator.clipboard.writeText(JSON.stringify(parameters, null, 2)) + setCopiedParams(true) + setTimeout(() => setCopiedParams(false), 2000) + } catch (err) { + console.error('Failed to copy to clipboard:', err) + } + } + + const handleCopyResult = async () => { + if (!result && !error) return + + try { + const contentToCopy = error || (typeof result === 'string' ? result : JSON.stringify(result, null, 2)) + await navigator.clipboard.writeText(contentToCopy) + setCopiedResult(true) + setTimeout(() => setCopiedResult(false), 2000) + } catch (err) { + console.error('Failed to copy to clipboard:', err) + } + } + + // Handle text selection detection + const handleMouseDown = () => { + setIsSelecting(true) + } + + const handleMouseUp = () => { + // Small delay to ensure selection is complete + setTimeout(() => { + const selection = window.getSelection() + const hasSelection = selection && selection.toString().length > 0 + setIsSelecting(hasSelection) + }, 50) + } + + const handleSelectionChange = () => { + const selection = window.getSelection() + const hasSelection = selection && selection.toString().length > 0 + setIsSelecting(hasSelection) + } + + // Enhanced mouse leave handler that respects text selection + const handleMouseLeaveWithSelection = () => { + if (!isSelecting && onMouseLeave) { + onMouseLeave() + } + } + + // Add event listener for selection changes + useEffect(() => { + document.addEventListener('selectionchange', handleSelectionChange) + return () => { + document.removeEventListener('selectionchange', handleSelectionChange) + } + }, []) // Calculate positioning - intelligently position based on available space const getSmartPosition = () => { const viewportWidth = window.innerWidth @@ -246,7 +310,9 @@ export const ToolPreview: React.FC = ({ fontSize: '12px' }} onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} + onMouseLeave={handleMouseLeaveWithSelection} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} > {/* Header */}
= ({ color: 'var(--foreground)', marginBottom: '6px', textTransform: 'uppercase', - letterSpacing: '0.05em' + letterSpacing: '0.05em', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' }}> Parameters + {parameters && ( + + )}
{keyParams.map((param, index) => (
= ({ color: status === 'error' ? '#ef4444' : 'var(--foreground)', marginBottom: '6px', textTransform: 'uppercase', - letterSpacing: '0.05em' + letterSpacing: '0.05em', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' }}> {status === 'error' ? 'Error' : 'Result'} + {(result || error) && ( + + )}