diff --git a/apps/frontend/src/main/ipc-handlers/env-handlers.ts b/apps/frontend/src/main/ipc-handlers/env-handlers.ts index c0d7e2278e..171b36e5fc 100644 --- a/apps/frontend/src/main/ipc-handlers/env-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/env-handlers.ts @@ -11,6 +11,7 @@ import { parseEnvFile } from './utils'; import { getClaudeCliInvocation, getClaudeCliInvocationAsync } from '../claude-cli-utils'; import { debugError } from '../../shared/utils/debug-logger'; import { getSpawnOptions, getSpawnCommand } from '../env-utils'; +import { getMemoriesDir } from '../config-paths'; // GitLab environment variable keys const GITLAB_ENV_KEYS = { @@ -336,7 +337,7 @@ ${existingVars['OLLAMA_EMBEDDING_DIM'] ? `OLLAMA_EMBEDDING_DIM=${existingVars['O # LadybugDB Database (embedded - no Docker required) ${existingVars['GRAPHITI_DATABASE'] ? `GRAPHITI_DATABASE=${existingVars['GRAPHITI_DATABASE']}` : '# GRAPHITI_DATABASE=auto_claude_memory'} -${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : '# GRAPHITI_DB_PATH=~/.auto-claude/memories'} +${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : `# GRAPHITI_DB_PATH=${getMemoriesDir()}`} `; return content; diff --git a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts index 72d786a261..6b739d3848 100644 --- a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts @@ -413,6 +413,22 @@ export function registerMemoryHandlers(): void { } ); + // Get platform-specific memories directory path + ipcMain.handle( + IPC_CHANNELS.MEMORY_GET_DIR, + async (): Promise> => { + try { + const memoriesDir = getDefaultDbPath(); + return { success: true, data: memoriesDir }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get memories directory', + }; + } + } + ); + // Test memory database connection ipcMain.handle( IPC_CHANNELS.MEMORY_TEST_CONNECTION, diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 6efc625edf..5405a97f81 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -7,7 +7,7 @@ * LadybugDB stores data in Kuzu format at ~/.auto-claude/memories// */ -import { spawn } from 'child_process'; +import { spawn, spawnSync } from 'child_process'; import * as path from 'path'; import { fileURLToPath } from 'url'; import * as fs from 'fs'; @@ -612,7 +612,7 @@ export class MemoryService { if (!data.databaseExists) { return { success: false, - message: `Database not found at ${data.databasePath}/${data.database}`, + message: `Database not found at ${path.join(data.databasePath, data.database)}`, }; } @@ -744,11 +744,106 @@ export function isKuzuAvailable(): boolean { return scriptPath !== null; } +/** + * Check if LadybugDB (real_ladybug) Python package is installed + * Returns detailed status about the installation + */ +export interface LadybugInstallStatus { + installed: boolean; + pythonAvailable: boolean; + error?: string; +} + +let ladybugInstallCache: LadybugInstallStatus | null = null; + +/** + * Error key constants for i18n translation. + * These keys should be defined in errors.json translation files. + */ +export const LADYBUG_ERROR_KEYS = { + pythonNotFound: 'errors:ladybug.pythonNotFound', + notInstalled: 'errors:ladybug.notInstalled', + buildTools: 'errors:ladybug.buildTools', + checkFailed: 'errors:ladybug.checkFailed', +} as const; + +export function checkLadybugInstalled(): LadybugInstallStatus { + // Return cached result if available (avoid repeated slow checks) + if (ladybugInstallCache !== null) { + return ladybugInstallCache; + } + + const pythonCmd = findPythonCommand(); + if (!pythonCmd) { + ladybugInstallCache = { + installed: false, + pythonAvailable: false, + error: LADYBUG_ERROR_KEYS.pythonNotFound, + }; + return ladybugInstallCache; + } + + try { + const [cmd, args] = parsePythonCommand(pythonCmd); + const checkArgs = [...args, '-c', 'import real_ladybug; print("OK")']; + + // Use getMemoryPythonEnv() to ensure real_ladybug can be found + const pythonEnv = getMemoryPythonEnv(); + + const result = spawnSync(cmd, checkArgs, { + encoding: 'utf-8', + timeout: 10000, + windowsHide: true, + env: pythonEnv, + }); + + if (result.status === 0 && result.stdout?.includes('OK')) { + ladybugInstallCache = { + installed: true, + pythonAvailable: true, + }; + } else { + // Parse error to provide helpful message (using i18n keys) + const stderr = result.stderr || ''; + let error: string = LADYBUG_ERROR_KEYS.notInstalled; + + if (stderr.includes('ModuleNotFoundError') || stderr.includes('No module named')) { + error = LADYBUG_ERROR_KEYS.notInstalled; + } else if (stderr.includes('WinError 2') || stderr.includes('system cannot find')) { + error = LADYBUG_ERROR_KEYS.buildTools; + } + + ladybugInstallCache = { + installed: false, + pythonAvailable: true, + error, + }; + } + } catch (err) { + ladybugInstallCache = { + installed: false, + pythonAvailable: true, + error: LADYBUG_ERROR_KEYS.checkFailed, + }; + } + + return ladybugInstallCache; +} + +/** + * Clear the LadybugDB installation cache (useful after installation attempt) + */ +export function clearLadybugInstallCache(): void { + ladybugInstallCache = null; +} + /** * Get memory service status */ export interface MemoryServiceStatus { kuzuInstalled: boolean; + ladybugInstalled: boolean; + ladybugError?: string; databasePath: string; databaseExists: boolean; databases: string[]; @@ -765,8 +860,13 @@ export function getMemoryServiceStatus(dbPath?: string): MemoryServiceStatus { const pythonAvailable = findPythonCommand() !== null; const scriptAvailable = getQueryScriptPath() !== null; + // Check if LadybugDB is actually installed + const ladybugStatus = checkLadybugInstalled(); + return { kuzuInstalled: pythonAvailable && scriptAvailable, + ladybugInstalled: ladybugStatus.installed, + ladybugError: ladybugStatus.error, databasePath: basePath, databaseExists: databases.length > 0, databases, diff --git a/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts b/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts index 5126fd6045..7d580a1136 100644 --- a/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts +++ b/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts @@ -908,6 +908,71 @@ describe('claude-integration-handler - Helper Functions', () => { }); }); + describe('escapeShellCommand', () => { + it('should add & call operator with single quotes for PowerShell on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + // PowerShell needs & to execute commands with -- flags + // Without &, PowerShell interprets -- as the decrement operator + // Uses single quotes to prevent variable expansion + const result = escapeShellCommand('C:\\Users\\test\\claude.exe', 'powershell'); + expect(result).toBe("& 'C:\\Users\\test\\claude.exe'"); + }); + + it('should NOT add & call operator for cmd.exe on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('C:\\Users\\test\\claude.exe', 'cmd'); + expect(result).toBe('"C:\\Users\\test\\claude.exe"'); + expect(result).not.toContain('&'); + }); + + it('should default to cmd.exe style when shellType is undefined on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('C:\\Users\\test\\claude.exe'); + expect(result).toBe('"C:\\Users\\test\\claude.exe"'); + expect(result).not.toContain('&'); + }); + + it('should use single quotes on macOS', async () => { + mockPlatform('darwin'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('/usr/local/bin/claude'); + expect(result).toBe("'/usr/local/bin/claude'"); + }); + + it('should use single quotes on Linux', async () => { + mockPlatform('linux'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('/usr/local/bin/claude'); + expect(result).toBe("'/usr/local/bin/claude'"); + }); + + it('should escape embedded single quotes in PowerShell path', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + // Paths with single quotes should be escaped by doubling them + const result = escapeShellCommand("C:\\Users\\O'Brien\\claude.exe", 'powershell'); + expect(result).toBe("& 'C:\\Users\\O''Brien\\claude.exe'"); + }); + + it('should handle % characters in cmd.exe path', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + // Paths with % should be escaped for cmd.exe + const result = escapeShellCommand('C:\\Users\\test%user\\claude.exe', 'cmd'); + expect(result).toBe('"C:\\Users\\test%%user\\claude.exe"'); + }); + }); + describe('finalizeClaudeInvoke', () => { it('should set terminal title to "Claude" for default profile when terminal has default name', async () => { const { finalizeClaudeInvoke } = await import('../claude-integration-handler'); diff --git a/apps/frontend/src/main/terminal/claude-integration-handler.ts b/apps/frontend/src/main/terminal/claude-integration-handler.ts index 84846fc65f..ac96bc16b7 100644 --- a/apps/frontend/src/main/terminal/claude-integration-handler.ts +++ b/apps/frontend/src/main/terminal/claude-integration-handler.ts @@ -15,7 +15,7 @@ import * as OutputParser from './output-parser'; import * as SessionHandler from './session-handler'; import * as PtyManager from './pty-manager'; import { debugLog, debugError } from '../../shared/utils/debug-logger'; -import { escapeShellArg, escapeForWindowsDoubleQuote, buildCdCommand } from '../../shared/utils/shell-escape'; +import { escapeShellArg, escapeForWindowsDoubleQuote, buildCdCommand, type WindowsShellType } from '../../shared/utils/shell-escape'; import { getClaudeCliInvocation, getClaudeCliInvocationAsync } from '../claude-cli-utils'; import { isWindows } from '../platform'; import type { @@ -115,17 +115,28 @@ function normalizePathForBash(envPath: string): string { } /** - * Generate temp file content for OAuth token based on platform + * Generate temp file content for OAuth token based on platform and shell type * - * On Windows, creates a .bat file with set command using double-quote syntax; + * On Windows cmd.exe, creates a .bat file with set command using double-quote syntax; + * on Windows PowerShell, creates a .ps1 file with $env: syntax; * on Unix, creates a shell script with export. * * @param token - OAuth token value + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct syntax. * @returns Content string for the temp file */ -function generateTokenTempFileContent(token: string): string { +function generateTokenTempFileContent(token: string, shellType?: WindowsShellType): string { if (isWindows()) { - // Windows: Use double-quote syntax for set command to handle special characters + if (shellType === 'powershell') { + // PowerShell: Use $env: syntax with single quotes to prevent variable expansion + // Single quotes in PowerShell prevent $var interpolation; escape embedded single quotes by doubling + const escapedToken = token + .replace(/\r/g, '') + .replace(/\n/g, '') + .replace(/'/g, "''"); + return `$env:CLAUDE_CODE_OAUTH_TOKEN = '${escapedToken}'\r\n`; + } + // Windows cmd.exe: Use double-quote syntax for set command to handle special characters // Format: set "VARNAME=value" - quotes allow spaces and special chars in value // For values inside double quotes, use escapeForWindowsDoubleQuote() because // caret is literal inside double quotes in cmd.exe (only " needs escaping). @@ -137,30 +148,45 @@ function generateTokenTempFileContent(token: string): string { } /** - * Get the file extension for temp files based on platform + * Get the file extension for temp files based on platform and shell type * - * @returns File extension including the dot (e.g., '.bat' on Windows, '' on Unix) + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct extension. + * @returns File extension including the dot (e.g., '.bat' on Windows cmd, '.ps1' on PowerShell, '' on Unix) */ -function getTempFileExtension(): string { - return isWindows() ? '.bat' : ''; +function getTempFileExtension(shellType?: WindowsShellType): string { + if (isWindows()) { + return shellType === 'powershell' ? '.ps1' : '.bat'; + } + return ''; } /** * Build PATH environment variable prefix for Claude CLI invocation. * - * On Windows, uses semicolon separators and cmd.exe escaping. + * On Windows cmd.exe, uses semicolon separators and cmd.exe escaping. + * On Windows PowerShell, uses $env:PATH syntax with semicolon separators. * On Unix/macOS, uses colon separators and bash escaping. * * @param pathEnv - PATH environment variable value + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct syntax. * @returns Empty string if no PATH, otherwise platform-specific PATH prefix */ -function buildPathPrefix(pathEnv: string): string { +function buildPathPrefix(pathEnv: string, shellType?: WindowsShellType): string { if (!pathEnv) { return ''; } if (isWindows()) { - // Windows: Use semicolon-separated PATH with double-quote escaping + if (shellType === 'powershell') { + // PowerShell: Use $env:PATH syntax with single quotes to prevent variable expansion + // Single quotes in PowerShell prevent $var interpolation; escape embedded single quotes by doubling + const escapedPath = pathEnv + .replace(/\r/g, '') + .replace(/\n/g, '') + .replace(/'/g, "''"); + return `$env:PATH = '${escapedPath}'; `; + } + // Windows cmd.exe: Use semicolon-separated PATH with double-quote escaping // Format: set "PATH=value" where value uses semicolons // For values inside double quotes, use escapeForWindowsDoubleQuote() because // caret is literal inside double quotes in cmd.exe (only " needs escaping). @@ -177,18 +203,33 @@ function buildPathPrefix(pathEnv: string): string { /** * Escape a command for safe use in shell commands. * - * On Windows, wraps in double quotes for cmd.exe. Since the value is inside + * On Windows cmd.exe, wraps in double quotes. Since the value is inside * double quotes, we use escapeForWindowsDoubleQuote() (only escapes embedded * double quotes as ""). Caret escaping is NOT used inside double quotes. + * + * For PowerShell, uses single quotes to prevent variable expansion and backtick + * interpretation, with the call operator (&) to handle -- flags correctly. + * * On Unix/macOS, wraps in single quotes for bash. * * @param cmd - The command to escape + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct syntax. * @returns The escaped command safe for use in shell commands */ -function escapeShellCommand(cmd: string): string { +export function escapeShellCommand(cmd: string, shellType?: WindowsShellType): string { if (isWindows()) { - // Windows: Wrap in double quotes and escape only embedded double quotes - // Inside double quotes, caret is literal, so use escapeForWindowsDoubleQuote() + if (shellType === 'powershell') { + // PowerShell: Use single quotes to prevent variable expansion ($var) and + // backtick interpretation. Escape embedded single quotes by doubling them. + // Use call operator (&) so PowerShell doesn't interpret -- as decrement. + const escapedCmd = cmd + .replace(/\r/g, '') + .replace(/\n/g, '') + .replace(/'/g, "''"); + return `& '${escapedCmd}'`; + } + + // cmd.exe: Wrap in double quotes and escape embedded double quotes/percents const escapedCmd = escapeForWindowsDoubleQuote(cmd); return `"${escapedCmd}"`; } @@ -217,6 +258,17 @@ type ClaudeCommandConfig = | { method: 'temp-file'; tempFile: string } | { method: 'config-dir'; configDir: string }; +/** + * Escape a path for PowerShell single-quoted strings. + * Single quotes in PowerShell prevent variable expansion; embedded single quotes are doubled. + */ +function escapePowerShellPath(path: string): string { + return path + .replace(/\r/g, '') + .replace(/\n/g, '') + .replace(/'/g, "''"); +} + /** * Build the shell command for invoking Claude CLI. * @@ -228,14 +280,15 @@ type ClaudeCommandConfig = * All non-default methods include history-safe prefixes (HISTFILE=, HISTCONTROL=) * to prevent sensitive data from appearing in shell history (Unix/macOS only). * - * On Windows, uses cmd.exe/PowerShell compatible syntax without bash-specific commands. - * The temp file method on Windows uses a batch file approach with inline environment setup. + * On Windows cmd.exe, uses batch file approach with inline environment setup. + * On Windows PowerShell, uses . (dot sourcing) for .ps1 files and $env: syntax. * * @param cwdCommand - Command to change directory (empty string if no change needed) * @param pathPrefix - PATH prefix for Claude CLI (empty string if not needed) * @param escapedClaudeCmd - Shell-escaped Claude CLI command * @param config - Configuration object with method and required options (discriminated union) * @param extraFlags - Optional extra flags to append to the command (e.g., '--dangerously-skip-permissions') + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct syntax. * @returns Complete shell command string ready for terminal.pty.write() * * @example @@ -247,24 +300,37 @@ type ClaudeCommandConfig = * buildClaudeShellCommand('', '', 'claude', { method: 'temp-file', tempFile: '/tmp/token' }); * // Returns: 'clear && HISTFILE= HISTCONTROL=ignorespace bash -c "source /tmp/token && rm -f /tmp/token && exec claude"\r' * - * // Temp file method (Windows) + * // Temp file method (Windows cmd.exe) * buildClaudeShellCommand('', '', 'claude.cmd', { method: 'temp-file', tempFile: 'C:\\Users\\...\\token.bat' }); * // Returns: 'cls && call C:\\Users\\...\\token.bat && claude.cmd\r' + * + * // Temp file method (Windows PowerShell) + * buildClaudeShellCommand('', '', '& claude.cmd', { method: 'temp-file', tempFile: 'C:\\Users\\...\\token.ps1' }, undefined, 'powershell'); + * // Returns: 'cls; cd /d "..."; . 'C:\\...\\token.ps1'; Remove-Item '...'; & claude.cmd\r' */ export function buildClaudeShellCommand( cwdCommand: string, pathPrefix: string, escapedClaudeCmd: string, config: ClaudeCommandConfig, - extraFlags?: string + extraFlags?: string, + shellType?: WindowsShellType ): string { const fullCmd = extraFlags ? `${escapedClaudeCmd}${extraFlags}` : escapedClaudeCmd; const isWin = isWindows(); + const isPowerShell = shellType === 'powershell'; switch (config.method) { case 'temp-file': if (isWin) { - // Windows: Use batch file approach with 'call' command + if (isPowerShell) { + // PowerShell: Use dot sourcing (.) to run the .ps1 file in current scope, + // then Remove-Item to delete it. Use single quotes for paths. + // PowerShell uses ; as command separator. + const escapedTempFile = escapePowerShellPath(config.tempFile); + return `cls; ${cwdCommand}${pathPrefix}. '${escapedTempFile}'; Remove-Item -Path '${escapedTempFile}' -Force; ${fullCmd}\r`; + } + // Windows cmd.exe: Use batch file approach with 'call' command // The temp file on Windows is a .bat file that sets CLAUDE_CODE_OAUTH_TOKEN // We use 'cls' instead of 'clear', and 'call' to execute the batch file // @@ -285,7 +351,12 @@ export function buildClaudeShellCommand( case 'config-dir': if (isWin) { - // Windows: Set environment variable using double-quote syntax + if (isPowerShell) { + // PowerShell: Set environment variable using $env: syntax with single quotes + const escapedConfigDir = escapePowerShellPath(config.configDir); + return `cls; ${cwdCommand}$env:CLAUDE_CONFIG_DIR = '${escapedConfigDir}'; ${pathPrefix}${fullCmd}\r`; + } + // Windows cmd.exe: Set environment variable using double-quote syntax // For values inside double quotes (set "VAR=value"), use // escapeForWindowsDoubleQuote() because caret is literal inside // double quotes in cmd.exe (only double quotes need escaping). @@ -842,13 +913,16 @@ function executeProfileCommand(options: ExecuteProfileCommandOptions): boolean { // Prefer configDir over token because CLAUDE_CONFIG_DIR lets Claude Code // read full Keychain credentials including subscriptionType ("max") and rateLimitTier. // Using CLAUDE_CODE_OAUTH_TOKEN alone lacks tier info, causing "Claude API" display. + const shellType = terminal.shellType; + if (activeProfile.configDir) { const command = buildClaudeShellCommand( cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'config-dir', configDir: activeProfile.configDir }, - extraFlags + extraFlags, + shellType ); debugLog(`${logPrefix} Executing command (configDir method, history-safe)`); PtyManager.writeToPty(terminal, command); @@ -868,17 +942,18 @@ function executeProfileCommand(options: ExecuteProfileCommandOptions): boolean { const nonce = crypto.randomBytes(8).toString('hex'); const tempFile = path.join( os.tmpdir(), - `.claude-token-${Date.now()}-${nonce}${getTempFileExtension()}` + `.claude-token-${Date.now()}-${nonce}${getTempFileExtension(shellType)}` ); debugLog(`${logPrefix} Writing token to temp file:`, tempFile); - fs.writeFileSync(tempFile, generateTokenTempFileContent(token), { mode: 0o600 }); + fs.writeFileSync(tempFile, generateTokenTempFileContent(token, shellType), { mode: 0o600 }); const command = buildClaudeShellCommand( cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', tempFile }, - extraFlags + extraFlags, + shellType ); debugLog(`${logPrefix} Executing command (temp file method, history-safe)`); PtyManager.writeToPty(terminal, command); @@ -917,6 +992,8 @@ async function executeProfileCommandAsync(options: ExecuteProfileCommandOptions) return false; // Use default method } + const shellType = terminal.shellType; + // Prefer configDir over token because CLAUDE_CONFIG_DIR lets Claude Code // read full Keychain credentials including subscriptionType ("max") and rateLimitTier. // Using CLAUDE_CODE_OAUTH_TOKEN alone lacks tier info, causing "Claude API" display. @@ -926,7 +1003,8 @@ async function executeProfileCommandAsync(options: ExecuteProfileCommandOptions) pathPrefix, escapedClaudeCmd, { method: 'config-dir', configDir: activeProfile.configDir }, - extraFlags + extraFlags, + shellType ); debugLog(`${logPrefix} Executing command (configDir method, history-safe)`); PtyManager.writeToPty(terminal, command); @@ -946,17 +1024,18 @@ async function executeProfileCommandAsync(options: ExecuteProfileCommandOptions) const nonce = crypto.randomBytes(8).toString('hex'); const tempFile = path.join( os.tmpdir(), - `.claude-token-${Date.now()}-${nonce}${getTempFileExtension()}` + `.claude-token-${Date.now()}-${nonce}${getTempFileExtension(shellType)}` ); debugLog(`${logPrefix} Writing token to temp file:`, tempFile); - await fsPromises.writeFile(tempFile, generateTokenTempFileContent(token), { mode: 0o600 }); + await fsPromises.writeFile(tempFile, generateTokenTempFileContent(token, shellType), { mode: 0o600 }); const command = buildClaudeShellCommand( cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', tempFile }, - extraFlags + extraFlags, + shellType ); debugLog(`${logPrefix} Executing command (temp file method, history-safe)`); PtyManager.writeToPty(terminal, command); @@ -1019,10 +1098,11 @@ export function invokeClaude( isDefault: activeProfile?.isDefault }); - const cwdCommand = buildCdCommand(cwd, terminal.shellType); + const shellType = terminal.shellType; + const cwdCommand = buildCdCommand(cwd, shellType); const { command: claudeCmd, env: claudeEnv } = getClaudeCliInvocation(); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); - const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, shellType); + const pathPrefix = buildPathPrefix(claudeEnv.PATH || '', shellType); const needsEnvOverride: boolean = !!(profileId && profileId !== previousProfileId); debugLog('[ClaudeIntegration:invokeClaude] Environment override check:', { @@ -1057,7 +1137,7 @@ export function invokeClaude( debugLog('[ClaudeIntegration:invokeClaude] Using terminal environment for non-default profile:', activeProfile.name); } - const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags); + const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags, shellType); debugLog('[ClaudeIntegration:invokeClaude] Executing command (default method):', command); PtyManager.writeToPty(terminal, command); @@ -1107,9 +1187,10 @@ export function resumeClaude( terminal.isClaudeMode = true; SessionHandler.releaseSessionId(terminal.id); + const shellType = terminal.shellType; const { command: claudeCmd, env: claudeEnv } = getClaudeCliInvocation(); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); - const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, shellType); + const pathPrefix = buildPathPrefix(claudeEnv.PATH || '', shellType); // Always use --continue which resumes the most recent session in the current directory. // This is more reliable than --resume with session IDs since Auto Claude already restores @@ -1212,7 +1293,8 @@ export async function invokeClaudeAsync( }); // Async CLI invocation - non-blocking - const cwdCommand = buildCdCommand(cwd, terminal.shellType); + const shellType = terminal.shellType; + const cwdCommand = buildCdCommand(cwd, shellType); // Add timeout protection for CLI detection (10s timeout) const cliInvocationPromise = getClaudeCliInvocationAsync(); @@ -1225,8 +1307,8 @@ export async function invokeClaudeAsync( if (timeoutId) clearTimeout(timeoutId); }); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); - const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, shellType); + const pathPrefix = buildPathPrefix(claudeEnv.PATH || '', shellType); const needsEnvOverride: boolean = !!(profileId && profileId !== previousProfileId); debugLog('[ClaudeIntegration:invokeClaudeAsync] Environment override check:', { @@ -1261,7 +1343,7 @@ export async function invokeClaudeAsync( debugLog('[ClaudeIntegration:invokeClaudeAsync] Using terminal environment for non-default profile:', activeProfile.name); } - const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags); + const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags, shellType); debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (default method):', command); PtyManager.writeToPty(terminal, command); @@ -1321,8 +1403,9 @@ export async function resumeClaudeAsync( if (timeoutId) clearTimeout(timeoutId); }); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); - const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); + const shellType = terminal.shellType; + const escapedClaudeCmd = escapeShellCommand(claudeCmd, shellType); + const pathPrefix = buildPathPrefix(claudeEnv.PATH || '', shellType); // Always use --continue which resumes the most recent session in the current directory. // This is more reliable than --resume with session IDs since Auto Claude already restores diff --git a/apps/frontend/src/preload/api/project-api.ts b/apps/frontend/src/preload/api/project-api.ts index 3852c9e440..70de7a7ec7 100644 --- a/apps/frontend/src/preload/api/project-api.ts +++ b/apps/frontend/src/preload/api/project-api.ts @@ -63,6 +63,7 @@ export interface ProjectAPI { getMemoryInfrastructureStatus: (dbPath?: string) => Promise>; listMemoryDatabases: (dbPath?: string) => Promise>; testMemoryConnection: (dbPath?: string, database?: string) => Promise>; + getMemoriesDir: () => Promise>; // Graphiti Validation Operations validateLLMApiKey: (provider: string, apiKey: string) => Promise>; @@ -223,6 +224,9 @@ export const createProjectAPI = (): ProjectAPI => ({ testMemoryConnection: (dbPath?: string, database?: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.MEMORY_TEST_CONNECTION, dbPath, database), + getMemoriesDir: (): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.MEMORY_GET_DIR), + // Graphiti Validation Operations validateLLMApiKey: (provider: string, apiKey: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_VALIDATE_LLM, provider, apiKey), diff --git a/apps/frontend/src/renderer/components/context/MemoriesTab.tsx b/apps/frontend/src/renderer/components/context/MemoriesTab.tsx index 736a01b065..52b1bd9e5f 100644 --- a/apps/frontend/src/renderer/components/context/MemoriesTab.tsx +++ b/apps/frontend/src/renderer/components/context/MemoriesTab.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { RefreshCw, Database, @@ -21,6 +22,7 @@ import { cn } from '../../lib/utils'; import { MemoryCard } from './MemoryCard'; import { InfoItem } from './InfoItem'; import { memoryFilterCategories } from './constants'; +import { useMemoriesDir } from '../../hooks/useMemoriesDir'; import type { GraphitiMemoryStatus, GraphitiMemoryState, MemoryEpisode } from '../../../shared/types'; type FilterCategory = keyof typeof memoryFilterCategories; @@ -77,9 +79,13 @@ export function MemoriesTab({ searchLoading, onSearch }: MemoriesTabProps) { + const { t } = useTranslation(['settings', 'errors']); const [localSearchQuery, setLocalSearchQuery] = useState(''); const [activeFilter, setActiveFilter] = useState('all'); + // Platform-specific memories directory path (extracted hook) + const memoriesDir = useMemoriesDir(); + // Calculate memory counts by category const memoryCounts = useMemo(() => { const counts: Record = { @@ -145,8 +151,8 @@ export function MemoriesTab({ {memoryStatus?.available ? ( <>
- - + +
{/* Memory Stats Summary */} @@ -183,7 +189,7 @@ export function MemoriesTab({ ) : (
-

{memoryStatus?.reason || 'Graphiti memory is not configured'}

+

{memoryStatus?.reason || t('errors:graphiti_not_configured')}

To enable graph memory, set GRAPHITI_ENABLED=true in project settings.

diff --git a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx index 4b4d1bccb4..7f4a54cb9e 100644 --- a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Brain, Database, @@ -25,6 +26,7 @@ import { SelectValue } from '../ui/select'; import { useSettingsStore } from '../../stores/settings-store'; +import { useMemoriesDir } from '../../hooks/useMemoriesDir'; import type { GraphitiLLMProvider, GraphitiEmbeddingProvider, AppSettings } from '../../../shared/types'; interface GraphitiStepProps { @@ -109,6 +111,7 @@ interface ValidationStatus { * Allows users to configure Graphiti memory backend with multiple provider options. */ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { + const { t } = useTranslation(['onboarding', 'errors']); const { settings, updateSettings } = useSettingsStore(); const [config, setConfig] = useState({ enabled: true, // Enabled by default for better first-time experience @@ -141,21 +144,31 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const [success, setSuccess] = useState(false); const [isCheckingInfra, setIsCheckingInfra] = useState(true); const [kuzuAvailable, setKuzuAvailable] = useState(null); + const [ladybugInstalled, setLadybugInstalled] = useState(null); + const [ladybugError, setLadybugError] = useState(null); const [isValidating, setIsValidating] = useState(false); const [validationStatus, setValidationStatus] = useState({ database: null, provider: null }); + // Platform-specific memories directory path (extracted hook) + const memoriesDir = useMemoriesDir(); + // Check LadybugDB/Kuzu availability on mount useEffect(() => { const checkInfrastructure = async () => { setIsCheckingInfra(true); try { const result = await window.electronAPI.getMemoryInfrastructureStatus(); - setKuzuAvailable(result?.success && result?.data?.memory?.kuzuInstalled ? true : false); - } catch { + const memory = result?.data?.memory; + setKuzuAvailable(result?.success && memory?.kuzuInstalled ? true : false); + setLadybugInstalled(memory?.ladybugInstalled ?? null); + setLadybugError(memory?.ladybugError ?? null); + } catch (err) { setKuzuAvailable(false); + setLadybugInstalled(false); + setLadybugError(t('onboarding:ladybug.checkFailed')); } finally { setIsCheckingInfra(false); } @@ -805,8 +818,41 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { )} - {/* Kuzu status notice */} - {kuzuAvailable === false && ( + {/* LadybugDB installation status */} + {ladybugInstalled === false && ladybugError && ( + + +
+ +
+

+ {t('onboarding:ladybug.notInstalled')} +

+

+ {ladybugError.startsWith('errors:') ? t(ladybugError) : ladybugError} +

+ {(ladybugError.includes('Visual Studio Build Tools') || ladybugError === 'errors:ladybug.buildTools') && ( + + + {t('onboarding:ladybug.downloadBuildTools')} + + )} +

+ {t('onboarding:ladybug.restartAfterInstall')} +

+
+
+
+
+ )} + + {/* Database will be created notice (when LadybugDB is installed but no DB yet) */} + {ladybugInstalled === true && kuzuAvailable === false && (
@@ -825,6 +871,25 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { )} + {/* LadybugDB ready notice */} + {ladybugInstalled === true && kuzuAvailable === true && ( + + +
+ +
+

+ LadybugDB Ready +

+

+ Memory database is installed and available. +

+
+
+
+
+ )} + {/* Info card about Graphiti */} @@ -919,7 +984,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { disabled={isSaving || isValidating} />

- Stored in ~/.auto-claude/graphs/ + Stored in {memoriesDir || 'memories directory'}

diff --git a/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx b/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx index 2e889c3a57..bcd27ba76f 100644 --- a/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx +++ b/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx @@ -1,4 +1,4 @@ -import { Loader2, CheckCircle2, AlertCircle, Database } from 'lucide-react'; +import { Loader2, CheckCircle2, AlertCircle, Database, ExternalLink } from 'lucide-react'; import type { InfrastructureStatus as InfrastructureStatusType } from '../../../shared/types'; interface InfrastructureStatusProps { @@ -14,6 +14,9 @@ export function InfrastructureStatus({ infrastructureStatus, isCheckingInfrastructure, }: InfrastructureStatusProps) { + const ladybugInstalled = infrastructureStatus?.memory.ladybugInstalled; + const ladybugError = infrastructureStatus?.memory.ladybugError; + return (
@@ -23,25 +26,43 @@ export function InfrastructureStatus({ )}
- {/* Kuzu Installation Status */} + {/* LadybugDB Installation Status */}
- {infrastructureStatus?.memory.kuzuInstalled ? ( + {ladybugInstalled ? ( ) : ( )} - Kuzu Database + LadybugDB
- {infrastructureStatus?.memory.kuzuInstalled ? ( + {ladybugInstalled ? ( Installed ) : ( - Not Available + Not Installed )}
+ {/* LadybugDB Error Details */} + {!ladybugInstalled && ladybugError && ( +
+

{ladybugError}

+ {ladybugError.includes('Visual Studio Build Tools') && ( + + + Download Visual Studio Build Tools + + )} +
+ )} + {/* Database Status */}
@@ -55,10 +76,10 @@ export function InfrastructureStatus({
{infrastructureStatus?.memory.databaseExists ? ( Ready - ) : infrastructureStatus?.memory.kuzuInstalled ? ( + ) : ladybugInstalled ? ( Will be created on first use ) : ( - Requires Kuzu + Requires LadybugDB )}
@@ -76,7 +97,7 @@ export function InfrastructureStatus({ Graph memory is ready to use
- ) : infrastructureStatus && !infrastructureStatus.memory.kuzuInstalled && ( + ) : infrastructureStatus && !ladybugInstalled && (

Graph memory requires Python 3.12+ with LadybugDB. No Docker needed.

diff --git a/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx b/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx index 6db71c43f3..58a5fd95fa 100644 --- a/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx +++ b/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx @@ -9,6 +9,7 @@ import { Switch } from '../ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Separator } from '../ui/separator'; import { Button } from '../ui/button'; +import { useMemoriesDir } from '../../hooks/useMemoriesDir'; import type { ProjectEnvConfig, ProjectSettings, InfrastructureStatus as InfrastructureStatusType } from '../../../shared/types'; interface OllamaEmbeddingModel { @@ -48,6 +49,9 @@ export function MemoryBackendSection({ const [ollamaStatus, setOllamaStatus] = useState<'idle' | 'checking' | 'connected' | 'disconnected'>('idle'); const [ollamaError, setOllamaError] = useState(null); + // Platform-specific memories directory path (extracted hook) + const memoriesDir = useMemoriesDir(); + const embeddingProvider = envConfig.graphitiProviderConfig?.embeddingProvider || 'openai'; const ollamaBaseUrl = envConfig.graphitiProviderConfig?.ollamaBaseUrl || 'http://localhost:11434'; @@ -478,7 +482,7 @@ export function MemoryBackendSection({

- Name for the memory database (stored in ~/.auto-claude/memories/) + Name for the memory database (stored in {memoriesDir || 'memories directory'})

- Custom storage location. Default: ~/.auto-claude/memories/ + Custom storage location. Default: {memoriesDir || 'memories directory'}

onUpdateConfig({ graphitiDbPath: e.target.value || undefined })} /> diff --git a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx index 2e76234843..2d762dcffc 100644 --- a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx @@ -7,6 +7,7 @@ import { ChevronUp, Globe } from 'lucide-react'; +import { useMemoriesDir } from '../../hooks/useMemoriesDir'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Switch } from '../ui/switch'; @@ -54,6 +55,9 @@ export function SecuritySettings({ azure: false }); + // Platform-specific memories directory path (extracted hook) + const memoriesDir = useMemoriesDir(); + // Sync parent's showOpenAIKey prop to local state useEffect(() => { setShowApiKey(prev => ({ ...prev, openai: showOpenAIKey })); @@ -437,7 +441,7 @@ export function SecuritySettings({

- Stored in ~/.auto-claude/memories/ + Stored in {memoriesDir || 'memories directory'}

- Custom storage location. Default: ~/.auto-claude/memories/ + Custom storage location. Default: {memoriesDir || 'memories directory'}

updateEnvConfig({ graphitiDbPath: e.target.value || undefined })} /> diff --git a/apps/frontend/src/renderer/components/terminal/useXterm.ts b/apps/frontend/src/renderer/components/terminal/useXterm.ts index 856a32d068..5bf828095d 100644 --- a/apps/frontend/src/renderer/components/terminal/useXterm.ts +++ b/apps/frontend/src/renderer/components/terminal/useXterm.ts @@ -23,13 +23,37 @@ interface UseXtermOptions { onDimensionsReady?: (cols: number, rows: number) => void; } -// Debounce helper function -function debounce void>(fn: T, ms: number): T { +// Debounce helper function with cancel support +function debounce void>(fn: T, ms: number): T & { cancel: () => void } { let timeoutId: ReturnType | null = null; - return ((...args: unknown[]) => { + + const debounced = ((...args: unknown[]) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), ms); - }) as T; + }) as T & { cancel: () => void }; + + debounced.cancel = () => { + if (timeoutId) clearTimeout(timeoutId); + }; + + return debounced; +} + +/** + * Calculate optimal font size based on device pixel ratio (Windows DPI scaling). + * devicePixelRatio > 1 indicates high-DPI displays (125%, 150%, 200% scaling). + * Base size: 13px for 100% scaling (devicePixelRatio = 1). + * Adjust font size using square root of scale factor for smoother scaling. + * This makes the reduction less aggressive on high-DPI displays. + * For 150% scaling (1.5x), use 13 * (1 / sqrt(1.5)) ~ 10.6 logical pixels. + * Minimum of 11px to maintain readability. + */ +function getAdjustedFontSize(baseFontSize: number = 13): number { + const scaleFactor = window.devicePixelRatio || 1; + const adjustedFontSize = scaleFactor > 1 + ? Math.max(baseFontSize * (1 / Math.sqrt(scaleFactor)), 11) + : baseFontSize; + return Math.round(adjustedFontSize); } export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsReady }: UseXtermOptions) { @@ -41,6 +65,8 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea const isDisposedRef = useRef(false); const dimensionsReadyCalledRef = useRef(false); const [dimensions, setDimensions] = useState<{ cols: number; rows: number }>({ cols: 80, rows: 24 }); + // Track device pixel ratio for DPI change detection + const [dpr, setDpr] = useState(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1); // Initialize xterm.js UI useEffect(() => { @@ -49,7 +75,7 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea const xterm = new XTerm({ cursorBlink: true, cursorStyle: 'block', - fontSize: 13, + fontSize: getAdjustedFontSize(), fontFamily: 'var(--font-mono), "JetBrains Mono", Menlo, Monaco, "Courier New", monospace', lineHeight: 1.2, letterSpacing: 0, @@ -341,11 +367,14 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea if (container) { const resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(container); - return () => resizeObserver.disconnect(); + return () => { + handleResize.cancel(); + resizeObserver.disconnect(); + }; } }, [onDimensionsReady]); - // Listen for terminal refit events (triggered after drag-drop reorder) +// Listen for terminal refit events (triggered after drag-drop reorder) useEffect(() => { const handleRefitAll = () => { if (fitAddonRef.current && xtermRef.current && terminalRef.current) { @@ -363,6 +392,44 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea return () => window.removeEventListener('terminal-refit-all', handleRefitAll); }, []); + // Handle DPI/scale changes (Windows display scaling, moving between monitors) + // Uses matchMedia with { once: true } pattern - state update triggers re-registration + // This fixes the bug where only the first DPI change was detected + useEffect(() => { + // Guard for JSDOM/test environments where matchMedia is not available + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return; + } + + const handleDPIChange = debounce(() => { + if (xtermRef.current) { + const newScaleFactor = window.devicePixelRatio || 1; + + // Update font size dynamically using the helper + xtermRef.current.options.fontSize = getAdjustedFontSize(); + + // Trigger a fit to recalculate terminal dimensions with new font size + if (fitAddonRef.current && terminalRef.current) { + const rect = terminalRef.current.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + fitAddonRef.current.fit(); + } + } + + // Update state to trigger re-registration with new dpr + setDpr(newScaleFactor); + } + }, 200); + + const mediaQuery = window.matchMedia(`(resolution: ${dpr}dppx)`); + mediaQuery.addEventListener('change', handleDPIChange, { once: true }); + + return () => { + handleDPIChange.cancel(); + mediaQuery.removeEventListener('change', handleDPIChange); + }; + }, [dpr]); // Re-run when dpr changes + const fit = useCallback(() => { if (fitAddonRef.current && xtermRef.current) { fitAddonRef.current.fit(); diff --git a/apps/frontend/src/renderer/hooks/index.ts b/apps/frontend/src/renderer/hooks/index.ts index 5103f9e2ef..810e32dcee 100644 --- a/apps/frontend/src/renderer/hooks/index.ts +++ b/apps/frontend/src/renderer/hooks/index.ts @@ -8,3 +8,4 @@ export { } from './useResolvedAgentSettings'; export { useVirtualizedTree } from './useVirtualizedTree'; export { useTerminalProfileChange } from './useTerminalProfileChange'; +export { useMemoriesDir } from './useMemoriesDir'; diff --git a/apps/frontend/src/renderer/hooks/useMemoriesDir.ts b/apps/frontend/src/renderer/hooks/useMemoriesDir.ts new file mode 100644 index 0000000000..962bc878cb --- /dev/null +++ b/apps/frontend/src/renderer/hooks/useMemoriesDir.ts @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; + +/** + * Custom hook to fetch the platform-specific memories directory path. + * Reduces duplication across components that need this path. + */ +export function useMemoriesDir(): string { + const [memoriesDir, setMemoriesDir] = useState(''); + + useEffect(() => { + let isMounted = true; + + window.electronAPI.getMemoriesDir() + .then((result) => { + if (isMounted && result.success && result.data) { + setMemoriesDir(result.data); + } + }) + .catch((err) => { + if (isMounted) { + console.error('Failed to get memories directory:', err); + } + }); + + return () => { + isMounted = false; + }; + }, []); + + return memoriesDir; +} diff --git a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts index 81168fa011..21044bee3a 100644 --- a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts @@ -10,7 +10,8 @@ export const infrastructureMock = { data: { memory: { kuzuInstalled: true, - databasePath: '~/.auto-claude/graphs', + ladybugInstalled: true, + databasePath: '~/.auto-claude/memories', databaseExists: true, databases: ['auto_claude_memory'] }, @@ -32,6 +33,11 @@ export const infrastructureMock = { } }), + getMemoriesDir: async () => ({ + success: true, + data: '~/.auto-claude/memories' + }), + // LLM API Validation Operations validateLLMApiKey: async () => ({ success: true, diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 6b538ae8bd..da3b5f1c85 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -418,6 +418,7 @@ export const IPC_CHANNELS = { MEMORY_STATUS: 'memory:status', MEMORY_LIST_DATABASES: 'memory:listDatabases', MEMORY_TEST_CONNECTION: 'memory:testConnection', + MEMORY_GET_DIR: 'memory:getDir', // Get platform-specific memories directory path // Graphiti validation GRAPHITI_VALIDATE_LLM: 'graphiti:validateLlm', diff --git a/apps/frontend/src/shared/i18n/locales/en/errors.json b/apps/frontend/src/shared/i18n/locales/en/errors.json index 88c3b88075..a4dade767f 100644 --- a/apps/frontend/src/shared/i18n/locales/en/errors.json +++ b/apps/frontend/src/shared/i18n/locales/en/errors.json @@ -1,9 +1,16 @@ { + "graphiti_not_configured": "Graphiti memory is not configured", "task": { "parseImplementationPlan": "Failed to parse implementation_plan.json for {{specId}}: {{error}}", "jsonError": { "titleSuffix": "(JSON Error)", "description": "⚠️ JSON Parse Error: {{error}}\n\nThe implementation_plan.json file is malformed. Run the backend auto-fix or manually repair the file." } + }, + "ladybug": { + "pythonNotFound": "Python not found. Please install Python 3.12 or later.", + "notInstalled": "LadybugDB (real_ladybug) is not installed. On Windows, this may require Visual Studio Build Tools to compile.", + "buildTools": "Failed to build LadybugDB. Please install Visual Studio Build Tools with C++ workload.", + "checkFailed": "Failed to check LadybugDB installation status." } } diff --git a/apps/frontend/src/shared/i18n/locales/en/onboarding.json b/apps/frontend/src/shared/i18n/locales/en/onboarding.json index 7da4d386f5..66bc4b4f9d 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -239,5 +239,11 @@ "retry": "Retry", "fallbackNote": "Memory will still work with keyword search even without embeddings." } + }, + "ladybug": { + "notInstalled": "LadybugDB Not Installed", + "downloadBuildTools": "Download Visual Studio Build Tools", + "restartAfterInstall": "After installing build tools, restart the application to retry.", + "checkFailed": "Failed to check infrastructure status" } } diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index 6c1942baf7..cfe493a6a6 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -503,6 +503,14 @@ "description": "Select a project from the sidebar to configure its settings." } }, + "memory": { + "database": "Database", + "path": "Path", + "fallbacks": { + "auto_claude_memory": "auto_claude_memory", + "memories_directory": "memories directory" + } + }, "mcp": { "title": "MCP Server Overview", "titleWithProject": "MCP Server Overview for {{projectName}}", diff --git a/apps/frontend/src/shared/i18n/locales/fr/errors.json b/apps/frontend/src/shared/i18n/locales/fr/errors.json index 371925d9b7..49e24525a5 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/errors.json +++ b/apps/frontend/src/shared/i18n/locales/fr/errors.json @@ -1,9 +1,16 @@ { + "graphiti_not_configured": "La mémoire Graphiti n'est pas configurée", "task": { "parseImplementationPlan": "Échec de l'analyse du fichier implementation_plan.json pour {{specId}} : {{error}}", "jsonError": { "titleSuffix": "(Erreur JSON)", "description": "⚠️ Erreur d'analyse JSON : {{error}}\n\nLe fichier implementation_plan.json est malformé. Exécutez la correction automatique du backend ou réparez le fichier manuellement." } + }, + "ladybug": { + "pythonNotFound": "Python introuvable. Veuillez installer Python 3.12 ou une version ultérieure.", + "notInstalled": "LadybugDB (real_ladybug) n'est pas installé. Sur Windows, cela peut nécessiter Visual Studio Build Tools pour la compilation.", + "buildTools": "Échec de la compilation de LadybugDB. Veuillez installer Visual Studio Build Tools avec les outils C++.", + "checkFailed": "Échec de la vérification du statut d'installation de LadybugDB." } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json index bc7e5d5b1a..c9e30756b4 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json @@ -239,5 +239,11 @@ "retry": "Réessayer", "fallbackNote": "La mémoire fonctionnera toujours avec la recherche par mots-clés même sans embeddings." } + }, + "ladybug": { + "notInstalled": "LadybugDB non installé", + "downloadBuildTools": "Télécharger Visual Studio Build Tools", + "restartAfterInstall": "Après l'installation des outils de compilation, redémarrez l'application pour réessayer.", + "checkFailed": "Échec de la vérification du statut de l'infrastructure" } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 0fcee50e4f..024754647a 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -503,6 +503,14 @@ "description": "Sélectionnez un projet dans la barre latérale pour configurer ses paramètres." } }, + "memory": { + "database": "Base de données", + "path": "Chemin", + "fallbacks": { + "auto_claude_memory": "auto_claude_memory", + "memories_directory": "répertoire des mémoires" + } + }, "mcp": { "title": "Aperçu des serveurs MCP", "titleWithProject": "Aperçu des serveurs MCP pour {{projectName}}", diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 689422ad9b..ab15c4d1c7 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -398,6 +398,7 @@ export interface ElectronAPI { getMemoryInfrastructureStatus: (dbPath?: string) => Promise>; listMemoryDatabases: (dbPath?: string) => Promise>; testMemoryConnection: (dbPath?: string, database?: string) => Promise>; + getMemoriesDir: () => Promise>; // Graphiti validation operations validateLLMApiKey: (provider: string, apiKey: string) => Promise>; diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index a0bd234b4c..e90876700f 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -152,6 +152,8 @@ export interface GraphitiMemoryStatus { // Memory Infrastructure Types export interface MemoryDatabaseStatus { kuzuInstalled: boolean; + ladybugInstalled: boolean; + ladybugError?: string; databasePath: string; databaseExists: boolean; databases: string[];