From 3533e71462e31351f5741b11d353e007882efd1d Mon Sep 17 00:00:00 2001 From: StillKnotKnown Date: Fri, 16 Jan 2026 00:27:41 +0200 Subject: [PATCH] fix(frontend): support Windows shell commands in Claude CLI invocation (ACS-261) Fixes hang on Windows after "Environment override check" log by replacing hardcoded bash commands with platform-aware shell syntax. - Windows: Uses cmd.exe syntax (cls, call, set, del) with .bat temp files - Unix/macOS: Preserves existing bash syntax (clear, source, export, rm) - Adds error handling and 10s timeout protection in async invocation - Extracts helper functions for DRY: generateTokenTempFileContent(), getTempFileExtension() - Adds 7 Windows-specific tests (30 total tests passing) --- .../claude-integration-handler.test.ts | 187 +++++++++++- .../terminal/claude-integration-handler.ts | 269 +++++++++++------- 2 files changed, 356 insertions(+), 100 deletions(-) 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 8f44838aa4..e124b0ea06 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 @@ -1,7 +1,7 @@ import { writeFileSync } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { describe, expect, it, vi, beforeEach, beforeAll, afterAll } from 'vitest'; import type * as pty from '@lydell/node-pty'; import type { TerminalProcess } from '../types'; import { buildCdCommand } from '../../../shared/utils/shell-escape'; @@ -249,6 +249,60 @@ describe('claude-integration-handler', () => { nowSpy.mockRestore(); }); + it('uses Windows batch file syntax for temp token on Windows platform', async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + + try { + Object.defineProperty(process, 'platform', { value: 'win32' }); + + const command = 'C:\\Tools\\claude\\claude.cmd'; + const profileManager = { + getActiveProfile: vi.fn(), + getProfile: vi.fn(() => ({ + id: 'prof-win', + name: 'Work', + isDefault: false, + oauthToken: 'windows-token-value', + })), + getProfileToken: vi.fn(() => 'windows-token-value'), + markProfileUsed: vi.fn(), + }; + + mockGetClaudeCliInvocation.mockReturnValue({ + command, + env: { PATH: 'C:\\Tools\\claude;C:\\Windows' }, + }); + mockGetClaudeProfileManager.mockReturnValue(profileManager); + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(9999); + + const terminal = createMockTerminal({ id: 'term-win' }); + + const { invokeClaude } = await import('../claude-integration-handler'); + invokeClaude(terminal, 'C:\\Users\\test\\project', 'prof-win', () => null, vi.fn()); + + const tokenPath = vi.mocked(writeFileSync).mock.calls[0]?.[0] as string; + const tokenContents = vi.mocked(writeFileSync).mock.calls[0]?.[1] as string; + const tokenPrefix = path.join(tmpdir(), '.claude-token-9999-'); + expect(tokenPath).toMatch(new RegExp(`^${escapeForRegex(tokenPrefix)}[0-9a-f]{16}\\.bat$`)); + expect(tokenContents).toBe("@echo off\r\nset CLAUDE_CODE_OAUTH_TOKEN='windows-token-value'\r\n"); + const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string; + expect(written).toContain('cls && '); + expect(written).toContain(`call '${tokenPath}'`); + expect(written).toContain(`del '${tokenPath}'`); + expect(written).toContain(`'${command}'`); + expect(written).not.toContain('bash -c'); + expect(written).not.toContain('source'); + expect(profileManager.getProfile).toHaveBeenCalledWith('prof-win'); + expect(mockPersistSession).toHaveBeenCalledWith(terminal); + + nowSpy.mockRestore(); + } finally { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform); + } + } + }); + it('prefers the temp token flow when profile has both oauth token and config dir', async () => { const command = '/opt/claude/bin/claude'; const profileManager = { @@ -499,6 +553,137 @@ describe('claude-integration-handler - Helper Functions', () => { expect(result).not.toContain('cd '); expect(result).toContain("source '/tmp/.token'"); }); + + describe('Windows platform', () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + + beforeAll(() => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + }); + + afterAll(() => { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform); + } + }); + + it('should build temp-file method command with Windows batch file syntax', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + "cd 'C:\\Users\\test\\project' && ", + "PATH='C:\\Tools\\claude' ", + "'C:\\Tools\\claude\\claude.cmd'", + { method: 'temp-file', escapedTempFile: "'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'" } + ); + + expect(result).toContain('cls && '); + expect(result).toContain("cd 'C:\\Users\\test\\project' && "); + expect(result).toContain("PATH='C:\\Tools\\claude' "); + expect(result).toContain("call 'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'"); + expect(result).toContain("'C:\\Tools\\claude\\claude.cmd'"); + expect(result).toContain("del 'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'"); + expect(result).not.toContain('bash -c'); + expect(result).not.toContain('source'); + expect(result).not.toContain('clear'); + }); + + it('should build config-dir method command with Windows set syntax', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + "cd 'C:\\Users\\test\\project' && ", + "PATH='C:\\Tools\\claude' ", + "'C:\\Tools\\claude\\claude.cmd'", + { method: 'config-dir', escapedConfigDir: "'C:\\Users\\test\\.claude-work'" } + ); + + expect(result).toContain('cls && '); + expect(result).toContain("cd 'C:\\Users\\test\\project' && "); + expect(result).toContain("set CLAUDE_CONFIG_DIR='C:\\Users\\test\\.claude-work'"); + expect(result).toContain("PATH='C:\\Tools\\claude' "); + expect(result).toContain("'C:\\Tools\\claude\\claude.cmd'"); + expect(result).not.toContain('bash -c'); + expect(result).not.toContain('clear'); + expect(result).not.toContain('exec'); + }); + + it('should build default command without bash on Windows', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + "cd 'C:\\Users\\test\\project' && ", + "PATH='C:\\Tools\\claude' ", + "'C:\\Tools\\claude\\claude.cmd'", + { method: 'default' } + ); + + expect(result).toBe("cd 'C:\\Users\\test\\project' && PATH='C:\\Tools\\claude' 'C:\\Tools\\claude\\claude.cmd'\r"); + expect(result).not.toContain('bash'); + }); + + it('should build temp-file method without cwd on Windows', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + '', + '', + "'C:\\Tools\\claude\\claude.cmd'", + { method: 'temp-file', escapedTempFile: "'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'" } + ); + + expect(result).toContain('cls && '); + expect(result).not.toContain('cd '); + expect(result).toContain("call 'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'"); + expect(result).toContain("'C:\\Tools\\claude\\claude.cmd'"); + expect(result).toContain("del 'C:\\Users\\test\\AppData\\Local\\Temp\\token.bat'"); + }); + }); + + describe('Unix platform (non-Windows)', () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + + beforeAll(() => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + }); + + afterAll(() => { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform); + } + }); + + it('should build temp-file method command with bash syntax on Unix', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + "cd '/tmp/project' && ", + "PATH='/opt/bin' ", + "'/opt/bin/claude'", + { method: 'temp-file', escapedTempFile: "'/tmp/.token-123'" } + ); + + expect(result).toContain('clear && '); + expect(result).toContain('bash -c'); + expect(result).toContain("source '/tmp/.token-123'"); + expect(result).toContain("rm -f '/tmp/.token-123'"); + expect(result).toContain("exec '/opt/bin/claude'"); + expect(result).not.toContain('call'); + expect(result).not.toContain('cls'); + }); + + it('should build config-dir method command with bash syntax on Unix', async () => { + const { buildClaudeShellCommand } = await import('../claude-integration-handler'); + const result = buildClaudeShellCommand( + "cd '/tmp/project' && ", + "PATH='/opt/bin' ", + "'/opt/bin/claude'", + { method: 'config-dir', escapedConfigDir: "'/home/user/.claude-work'" } + ); + + expect(result).toContain('clear && '); + expect(result).toContain('bash -c'); + expect(result).toContain("CLAUDE_CONFIG_DIR='/home/user/.claude-work'"); + expect(result).toContain("exec '/opt/bin/claude'"); + expect(result).not.toContain('set'); + expect(result).not.toContain('cls'); + }); + }); }); describe('finalizeClaudeInvoke', () => { diff --git a/apps/frontend/src/main/terminal/claude-integration-handler.ts b/apps/frontend/src/main/terminal/claude-integration-handler.ts index 606386998c..f808477259 100644 --- a/apps/frontend/src/main/terminal/claude-integration-handler.ts +++ b/apps/frontend/src/main/terminal/claude-integration-handler.ts @@ -26,6 +26,29 @@ function normalizePathForBash(envPath: string): string { return process.platform === 'win32' ? envPath.replace(/;/g, ':') : envPath; } +/** + * Generate temp file content for OAuth token based on platform + * + * On Windows, creates a .bat file with set command; on Unix, creates a shell script with export. + * + * @param token - OAuth token value + * @returns Content string for the temp file + */ +function generateTokenTempFileContent(token: string): string { + return process.platform === 'win32' + ? `@echo off\r\nset CLAUDE_CODE_OAUTH_TOKEN=${escapeShellArg(token)}\r\n` + : `export CLAUDE_CODE_OAUTH_TOKEN=${escapeShellArg(token)}\n`; +} + +/** + * Get the file extension for temp files based on platform + * + * @returns File extension including the dot (e.g., '.bat' on Windows, '' on Unix) + */ +function getTempFileExtension(): string { + return process.platform === 'win32' ? '.bat' : ''; +} + /** * Flag for YOLO mode (skip all permission prompts) * Extracted as constant to ensure consistency across invokeClaude and invokeClaudeAsync @@ -54,7 +77,10 @@ type ClaudeCommandConfig = * - 'config-dir': Sets CLAUDE_CONFIG_DIR for custom profile location * * All non-default methods include history-safe prefixes (HISTFILE=, HISTCONTROL=) - * to prevent sensitive data from appearing in shell history. + * 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. * * @param cwdCommand - Command to change directory (empty string if no change needed) * @param pathPrefix - PATH prefix for Claude CLI (empty string if not needed) @@ -64,13 +90,17 @@ type ClaudeCommandConfig = * @returns Complete shell command string ready for terminal.pty.write() * * @example - * // Default method + * // Default method (Unix/macOS) * buildClaudeShellCommand('cd /path && ', 'PATH=/bin ', 'claude', { method: 'default' }); * // Returns: 'cd /path && PATH=/bin claude\r' * - * // Temp file method + * // Temp file method (Unix/macOS) * buildClaudeShellCommand('', '', 'claude', { method: 'temp-file', escapedTempFile: '/tmp/token' }); * // Returns: 'clear && HISTFILE= HISTCONTROL=ignorespace bash -c "source /tmp/token && rm -f /tmp/token && exec claude"\r' + * + * // Temp file method (Windows) + * buildClaudeShellCommand('', '', 'claude.cmd', { method: 'temp-file', escapedTempFile: 'C:\\Users\\...\\token.bat' }); + * // Returns: 'cls && call C:\\Users\\...\\token.bat && claude.cmd\r' */ export function buildClaudeShellCommand( cwdCommand: string, @@ -80,12 +110,29 @@ export function buildClaudeShellCommand( extraFlags?: string ): string { const fullCmd = extraFlags ? `${escapedClaudeCmd}${extraFlags}` : escapedClaudeCmd; + const isWindows = process.platform === 'win32'; + switch (config.method) { case 'temp-file': - return `clear && ${cwdCommand}HISTFILE= HISTCONTROL=ignorespace ${pathPrefix}bash -c "source ${config.escapedTempFile} && rm -f ${config.escapedTempFile} && exec ${fullCmd}"\r`; + if (isWindows) { + // Windows: 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 + // After execution, delete the temp file using 'del' command + return `cls && ${cwdCommand}${pathPrefix}call ${config.escapedTempFile} && ${fullCmd} && del ${config.escapedTempFile}\r`; + } else { + // Unix/macOS: Use bash with source command and history-safe prefixes + return `clear && ${cwdCommand}HISTFILE= HISTCONTROL=ignorespace ${pathPrefix}bash -c "source ${config.escapedTempFile} && rm -f ${config.escapedTempFile} && exec ${fullCmd}"\r`; + } case 'config-dir': - return `clear && ${cwdCommand}HISTFILE= HISTCONTROL=ignorespace CLAUDE_CONFIG_DIR=${config.escapedConfigDir} ${pathPrefix}bash -c "exec ${fullCmd}"\r`; + if (isWindows) { + // Windows: Set environment variable and execute command directly + return `cls && ${cwdCommand}set CLAUDE_CONFIG_DIR=${config.escapedConfigDir} && ${pathPrefix}${fullCmd}\r`; + } else { + // Unix/macOS: Use bash with config dir and history-safe prefixes + return `clear && ${cwdCommand}HISTFILE= HISTCONTROL=ignorespace CLAUDE_CONFIG_DIR=${config.escapedConfigDir} ${pathPrefix}bash -c "exec ${fullCmd}"\r`; + } default: return `${cwdCommand}${pathPrefix}${fullCmd}\r`; @@ -440,12 +487,12 @@ export function invokeClaude( if (token) { const nonce = crypto.randomBytes(8).toString('hex'); - const tempFile = path.join(os.tmpdir(), `.claude-token-${Date.now()}-${nonce}`); + const tempFile = path.join(os.tmpdir(), `.claude-token-${Date.now()}-${nonce}${getTempFileExtension()}`); const escapedTempFile = escapeShellArg(tempFile); debugLog('[ClaudeIntegration:invokeClaude] Writing token to temp file:', tempFile); fs.writeFileSync( tempFile, - `export CLAUDE_CODE_OAUTH_TOKEN=${escapeShellArg(token)}\n`, + generateTokenTempFileContent(token), { mode: 0o600 } ); @@ -550,6 +597,7 @@ export function resumeClaude( * * Safe to call from Electron main process without blocking the event loop. * Uses async CLI detection which doesn't block on subprocess calls. + * Includes error handling and timeout protection to prevent hangs. */ export async function invokeClaudeAsync( terminal: TerminalProcess, @@ -559,109 +607,132 @@ export async function invokeClaudeAsync( onSessionCapture: (terminalId: string, projectPath: string, startTime: number) => void, dangerouslySkipPermissions?: boolean ): Promise { - debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE START (async) =========='); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Terminal ID:', terminal.id); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Requested profile ID:', profileId); - debugLog('[ClaudeIntegration:invokeClaudeAsync] CWD:', cwd); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Dangerously skip permissions:', dangerouslySkipPermissions); - - // Compute extra flags for YOLO mode - const extraFlags = dangerouslySkipPermissions ? YOLO_MODE_FLAG : undefined; - - terminal.isClaudeMode = true; - // Store YOLO mode setting so it persists across profile switches - terminal.dangerouslySkipPermissions = dangerouslySkipPermissions; - SessionHandler.releaseSessionId(terminal.id); - terminal.claudeSessionId = undefined; - const startTime = Date.now(); - const projectPath = cwd || terminal.projectPath || terminal.cwd; - - // Ensure profile manager is initialized (async, yields to event loop) - const profileManager = await initializeClaudeProfileManager(); - const activeProfile = profileId - ? profileManager.getProfile(profileId) - : profileManager.getActiveProfile(); - - const previousProfileId = terminal.claudeProfileId; - terminal.claudeProfileId = activeProfile?.id; - - debugLog('[ClaudeIntegration:invokeClaudeAsync] Profile resolution:', { - previousProfileId, - newProfileId: activeProfile?.id, - profileName: activeProfile?.name, - hasOAuthToken: !!activeProfile?.oauthToken, - isDefault: activeProfile?.isDefault - }); - - // Async CLI invocation - non-blocking - const cwdCommand = buildCdCommand(cwd); - const { command: claudeCmd, env: claudeEnv } = await getClaudeCliInvocationAsync(); - const escapedClaudeCmd = escapeShellArg(claudeCmd); - const pathPrefix = claudeEnv.PATH - ? `PATH=${escapeShellArg(normalizePathForBash(claudeEnv.PATH))} ` - : ''; - const needsEnvOverride = profileId && profileId !== previousProfileId; + const INVOKE_TIMEOUT_MS = 15000; // 15 second timeout for entire invocation - debugLog('[ClaudeIntegration:invokeClaudeAsync] Environment override check:', { - profileIdProvided: !!profileId, - previousProfileId, - needsEnvOverride - }); + try { + debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE START (async) =========='); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Terminal ID:', terminal.id); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Requested profile ID:', profileId); + debugLog('[ClaudeIntegration:invokeClaudeAsync] CWD:', cwd); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Dangerously skip permissions:', dangerouslySkipPermissions); + + // Compute extra flags for YOLO mode + const extraFlags = dangerouslySkipPermissions ? YOLO_MODE_FLAG : undefined; + + terminal.isClaudeMode = true; + // Store YOLO mode setting so it persists across profile switches + terminal.dangerouslySkipPermissions = dangerouslySkipPermissions; + SessionHandler.releaseSessionId(terminal.id); + terminal.claudeSessionId = undefined; + + const projectPath = cwd || terminal.projectPath || terminal.cwd; + + // Ensure profile manager is initialized (async, yields to event loop) + const profileManager = await initializeClaudeProfileManager(); + const activeProfile = profileId + ? profileManager.getProfile(profileId) + : profileManager.getActiveProfile(); + + const previousProfileId = terminal.claudeProfileId; + terminal.claudeProfileId = activeProfile?.id; + + debugLog('[ClaudeIntegration:invokeClaudeAsync] Profile resolution:', { + previousProfileId, + newProfileId: activeProfile?.id, + profileName: activeProfile?.name, + hasOAuthToken: !!activeProfile?.oauthToken, + isDefault: activeProfile?.isDefault + }); - if (needsEnvOverride && activeProfile && !activeProfile.isDefault) { - const token = profileManager.getProfileToken(activeProfile.id); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Token retrieval:', { - hasToken: !!token, - tokenLength: token?.length + // Async CLI invocation - non-blocking + const cwdCommand = buildCdCommand(cwd); + + // Add timeout protection for CLI detection (10s timeout) + const cliInvocationPromise = getClaudeCliInvocationAsync(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('CLI invocation timeout after 10s')), 10000) + ); + const { command: claudeCmd, env: claudeEnv } = await Promise.race([cliInvocationPromise, timeoutPromise]); + + const escapedClaudeCmd = escapeShellArg(claudeCmd); + const pathPrefix = claudeEnv.PATH + ? `PATH=${escapeShellArg(normalizePathForBash(claudeEnv.PATH))} ` + : ''; + const needsEnvOverride = profileId && profileId !== previousProfileId; + + debugLog('[ClaudeIntegration:invokeClaudeAsync] Environment override check:', { + profileIdProvided: !!profileId, + previousProfileId, + needsEnvOverride }); - if (token) { - const nonce = crypto.randomBytes(8).toString('hex'); - const tempFile = path.join(os.tmpdir(), `.claude-token-${Date.now()}-${nonce}`); - const escapedTempFile = escapeShellArg(tempFile); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Writing token to temp file:', tempFile); - await fsPromises.writeFile( - tempFile, - `export CLAUDE_CODE_OAUTH_TOKEN=${escapeShellArg(token)}\n`, - { mode: 0o600 } - ); + if (needsEnvOverride && activeProfile && !activeProfile.isDefault) { + const token = profileManager.getProfileToken(activeProfile.id); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Token retrieval:', { + hasToken: !!token, + tokenLength: token?.length + }); + + if (token) { + const nonce = crypto.randomBytes(8).toString('hex'); + const tempFile = path.join(os.tmpdir(), `.claude-token-${Date.now()}-${nonce}${getTempFileExtension()}`); + const escapedTempFile = escapeShellArg(tempFile); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Writing token to temp file:', tempFile); + await fsPromises.writeFile( + tempFile, + generateTokenTempFileContent(token), + { mode: 0o600 } + ); + + const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', escapedTempFile }, extraFlags); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (temp file method, history-safe)'); + terminal.pty.write(command); + profileManager.markProfileUsed(activeProfile.id); + finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); + debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (temp file) =========='); + return; + } else if (activeProfile.configDir) { + const escapedConfigDir = escapeShellArg(activeProfile.configDir); + const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'config-dir', escapedConfigDir }, extraFlags); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (configDir method, history-safe)'); + terminal.pty.write(command); + profileManager.markProfileUsed(activeProfile.id); + finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); + debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (configDir) =========='); + return; + } else { + debugLog('[ClaudeIntegration:invokeClaudeAsync] WARNING: No token or configDir available for non-default profile'); + } + } - const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', escapedTempFile }, extraFlags); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (temp file method, history-safe)'); - terminal.pty.write(command); - profileManager.markProfileUsed(activeProfile.id); - finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); - debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (temp file) =========='); - return; - } else if (activeProfile.configDir) { - const escapedConfigDir = escapeShellArg(activeProfile.configDir); - const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'config-dir', escapedConfigDir }, extraFlags); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (configDir method, history-safe)'); - terminal.pty.write(command); - profileManager.markProfileUsed(activeProfile.id); - finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); - debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (configDir) =========='); - return; - } else { - debugLog('[ClaudeIntegration:invokeClaudeAsync] WARNING: No token or configDir available for non-default profile'); + if (activeProfile && !activeProfile.isDefault) { + debugLog('[ClaudeIntegration:invokeClaudeAsync] Using terminal environment for non-default profile:', activeProfile.name); } - } - if (activeProfile && !activeProfile.isDefault) { - debugLog('[ClaudeIntegration:invokeClaudeAsync] Using terminal environment for non-default profile:', activeProfile.name); - } + const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags); + debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (default method):', command); + terminal.pty.write(command); - const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags); - debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (default method):', command); - terminal.pty.write(command); + if (activeProfile) { + profileManager.markProfileUsed(activeProfile.id); + } - if (activeProfile) { - profileManager.markProfileUsed(activeProfile.id); + finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); + debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (default) =========='); + } catch (error) { + const elapsed = Date.now() - startTime; + debugError('[ClaudeIntegration:invokeClaudeAsync] Invocation failed:', error); + debugError('[ClaudeIntegration:invokeClaudeAsync] Error details:', { + terminalId: terminal.id, + profileId, + cwd, + elapsedMs: elapsed, + errorName: error instanceof Error ? error.name : 'Unknown', + errorMessage: error instanceof Error ? error.message : String(error) + }); + throw error; // Re-throw to allow caller to handle } - - finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture); - debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (default) =========='); } /**