From 97486e62ec24f7640f251da523992c8e3c167caa Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Wed, 2 Sep 2026 22:48:45 -0700 Subject: [PATCH] fix(claude): scrub SDK child env and localize pending launch --- .../claude-child-process-environment.test.ts | 19 ++++++++ .../claude-child-process-environment.ts | 31 +++++++++---- .../claude-stream-json-connection.test.ts | 7 ++- .../claude/claude-stream-json-connection.ts | 2 +- .../tab-bar/QuickLaunchButton.test.ts | 44 +++++++++++++++---- .../components/tab-bar/QuickLaunchButton.tsx | 13 +++--- src/renderer/src/i18n/locales/en.json | 3 +- 7 files changed, 94 insertions(+), 25 deletions(-) diff --git a/src/main/claude/claude-child-process-environment.test.ts b/src/main/claude/claude-child-process-environment.test.ts index 5f2ec48fdd2..8299fdcdd61 100644 --- a/src/main/claude/claude-child-process-environment.test.ts +++ b/src/main/claude/claude-child-process-environment.test.ts @@ -41,4 +41,23 @@ describe('Claude child process environment', () => { SAFE_VALUE: 'preserved' }) }) + + it('can strip child-session stamps reintroduced by a full SDK launch overlay', () => { + expect( + buildClaudeChildProcessEnv( + { + CLAUDE_CODE_CHILD_SESSION: 'configured-child-session', + CLAUDE_CODE_SESSION_ID: 'configured-session', + CLAUDE_CODE_BRIDGE_SESSION_ID: 'configured-bridge-session' + }, + { + scrubConfiguredChildSessionStamps: true, + inheritedEnv: { + CLAUDE_CODE_CHILD_SESSION: 'inherited-child-session', + SAFE_VALUE: 'preserved' + } + } + ) + ).toEqual({ SAFE_VALUE: 'preserved' }) + }) }) diff --git a/src/main/claude/claude-child-process-environment.ts b/src/main/claude/claude-child-process-environment.ts index 164d9e686fc..58f00c5c1b8 100644 --- a/src/main/claude/claude-child-process-environment.ts +++ b/src/main/claude/claude-child-process-environment.ts @@ -16,9 +16,27 @@ function cloneProcessEnv(source: NodeJS.ProcessEnv): Record { return env } +function stripClaudeChildSessionStamps( + env: Record, + platform: NodeJS.Platform +): Record { + for (const key of CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS) { + for (const envKey of Object.keys(env)) { + if (envKey === key || (platform === 'win32' && envKey.toUpperCase() === key)) { + delete env[envKey] + } + } + } + return env +} + export function buildClaudeChildProcessEnv( configuredEnv: Record = {}, - options: { inheritedEnv?: NodeJS.ProcessEnv; platform?: NodeJS.Platform } = {} + options: { + inheritedEnv?: NodeJS.ProcessEnv + platform?: NodeJS.Platform + scrubConfiguredChildSessionStamps?: boolean + } = {} ): Record { const inheritedEnv = options.inheritedEnv ?? process.env const platform = options.platform ?? process.platform @@ -43,12 +61,9 @@ export function buildClaudeChildProcessEnv( } } } - for (const key of CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS) { - for (const inheritedKey of Object.keys(env)) { - if (inheritedKey === key || (platform === 'win32' && inheritedKey.toUpperCase() === key)) { - delete env[inheritedKey] - } - } + if (options.scrubConfiguredChildSessionStamps) { + return stripClaudeChildSessionStamps({ ...env, ...configuredEnv }, platform) } - return Object.assign(env, configuredEnv) + stripClaudeChildSessionStamps(env, platform) + return { ...env, ...configuredEnv } } diff --git a/src/main/claude/claude-stream-json-connection.test.ts b/src/main/claude/claude-stream-json-connection.test.ts index 9360988c29f..a3678833692 100644 --- a/src/main/claude/claude-stream-json-connection.test.ts +++ b/src/main/claude/claude-stream-json-connection.test.ts @@ -150,7 +150,10 @@ describe('Claude stream-json connection', () => { launchFor(scenario, { CLAUDE_CONFIG_DIR: '/accounts/managed/home', ANTHROPIC_AUTH_TOKEN: 'configured-token', - ORCA_AGENT_SESSION_SPAWN_TOKEN: 'spawn-9' + ORCA_AGENT_SESSION_SPAWN_TOKEN: 'spawn-9', + CLAUDE_CODE_CHILD_SESSION: 'configured-child-session', + CLAUDE_CODE_SESSION_ID: 'configured-session', + CLAUDE_CODE_BRIDGE_SESSION_ID: 'configured-bridge-session' }) ) @@ -165,6 +168,8 @@ describe('Claude stream-json connection', () => { expect(env.ORCA_CONNECTION_MARKER).toBe('inherited') expect(env.ANTHROPIC_API_KEY).toBeUndefined() expect(env.CLAUDE_CODE_CHILD_SESSION).toBeUndefined() + expect(env.CLAUDE_CODE_SESSION_ID).toBeUndefined() + expect(env.CLAUDE_CODE_BRIDGE_SESSION_ID).toBeUndefined() // Two SDK mutations of the child env, pinned so a bump cannot change them unseen. expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('sdk-ts') expect(env.NODE_OPTIONS).toBeUndefined() diff --git a/src/main/claude/claude-stream-json-connection.ts b/src/main/claude/claude-stream-json-connection.ts index 8cb09e4386b..8e39947c4f7 100644 --- a/src/main/claude/claude-stream-json-connection.ts +++ b/src/main/claude/claude-stream-json-connection.ts @@ -107,7 +107,7 @@ export async function openClaudeStreamJsonConnection( cwd: launch.cwd, // Why env is never omitted: the SDK inherits process.env when it is, which is // exactly the ambient ANTHROPIC_* auth leak this lane already shipped once. - env: buildClaudeChildProcessEnv(launch.env), + env: buildClaudeChildProcessEnv(launch.env, { scrubConfiguredChildSessionStamps: true }), pathToClaudeCodeExecutable: launch.pathToClaudeCodeExecutable, spawnClaudeCodeProcess: spawner.spawn, ...(handlers.canUseTool ? { canUseTool: handlers.canUseTool } : {}), diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts b/src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts index 8ea48013abc..f5ef9414509 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts @@ -9,7 +9,9 @@ const { openSettingsPageMock, openSettingsTargetMock, useDetectedAgentsMock, - useStructuredAgentSessionCreateMock + useStructuredAgentSessionCreateMock, + useStructuredCodexLaunchStatusMock, + translateMock } = vi.hoisted(() => ({ shortcutLabelMock: vi.fn<() => string | null>(), storeState: { @@ -29,7 +31,20 @@ const { supported: true, creating: false, create: vi.fn(async () => true) - })) + })), + useStructuredCodexLaunchStatusMock: vi.fn(() => 'idle' as 'idle' | 'pending' | 'failed'), + translateMock: vi.fn((key: string, fallback: string, values?: Record): string => { + const template = + key === 'auto.components.tab.bar.QuickLaunchButton.ec2adf093e' + ? 'Launch {{value0}} in a new terminal' + : key === 'auto.components.tab.bar.QuickLaunchButton.startingCodexChat' + ? 'Starting Codex chat (localized)…' + : fallback + return Object.entries(values ?? {}).reduce( + (text, [name, value]) => text.replace(`{{${name}}}`, value), + template + ) + }) })) vi.mock('@/hooks/useDetectedAgents', () => ({ @@ -44,6 +59,10 @@ vi.mock('../native-chat/use-structured-agent-session-create', () => ({ useStructuredAgentSessionCreate: useStructuredAgentSessionCreateMock })) +vi.mock('@/lib/structured-agent-session-launch', () => ({ + useStructuredCodexLaunchStatus: useStructuredCodexLaunchStatusMock +})) + vi.mock('@/store', () => { const useAppStore = Object.assign( (selector: (state: typeof storeState) => unknown) => { @@ -85,13 +104,7 @@ vi.mock('@/components/ui/dropdown-menu', async () => { } }) -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, fallback: string, values?: Record) => - Object.entries(values ?? {}).reduce( - (text, [key, value]) => text.replace(`{{${key}}}`, value), - fallback - ) -})) +vi.mock('@/i18n/i18n', () => ({ translate: translateMock })) vi.mock('sonner', () => ({ toast: { @@ -128,6 +141,9 @@ beforeEach(() => { shortcutLabelMock.mockReturnValue(null) useDetectedAgentsMock.mockClear() useStructuredAgentSessionCreateMock.mockClear() + useStructuredCodexLaunchStatusMock.mockReset() + useStructuredCodexLaunchStatusMock.mockReturnValue('idle') + translateMock.mockClear() openSettingsPageMock.mockReset() openSettingsTargetMock.mockReset() storeState.settings.defaultTuiAgent = 'codex' @@ -227,6 +243,16 @@ describe('QuickLaunchAgentMenuItems', () => { expect(useStructuredAgentSessionCreateMock).toHaveBeenCalledWith('worktree-1', 'claude') expect(useStructuredAgentSessionCreateMock).toHaveBeenCalledWith('worktree-1', 'codex') }) + + it('localizes pending Codex text and interpolates the existing launch tooltip', () => { + useStructuredCodexLaunchStatusMock.mockReturnValue('pending') + + const html = renderAgentMenuItems() + + expect(html).toContain('Starting Codex chat (localized)…') + expect(html).toContain('title="Launch Codex in a new terminal"') + expect(html).not.toContain('{{value0}}') + }) }) describe('shouldShowLaunchWatchdogTimeout', () => { diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index 14a1f41c52e..d24165722c6 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -210,7 +210,12 @@ function QuickLaunchAgentMenuItemsInner({ : null const isStructuredCodexPending = agent === 'codex' && structuredCodexLaunchStatus === 'pending' - const menuLabel = isStructuredCodexPending ? 'Starting Codex chat…' : label + const menuLabel = isStructuredCodexPending + ? translate( + 'auto.components.tab.bar.QuickLaunchButton.startingCodexChat', + 'Starting Codex chat…' + ) + : label const showsDefaultAgentShortcut = newAgentShortcut !== null && defaultAgent !== 'blank' && agent === defaultAgent return ( @@ -221,10 +226,8 @@ function QuickLaunchAgentMenuItemsInner({ className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium" title={translate( 'auto.components.tab.bar.QuickLaunchButton.ec2adf093e', - isStructuredCodexPending - ? 'Starting Codex chat…' - : 'Launch {{value0}} in a new terminal', - isStructuredCodexPending ? undefined : { value0: label } + 'Launch {{value0}} in a new terminal', + { value0: label } )} > {isStructuredCodexPending ? ( diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index b1036c327ee..d5c7654be9b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -3330,7 +3330,8 @@ "8dea9b5cdf": "No enabled agents", "f378201fbd": "Could not create chat session", "a847779775": "Start {{value0}} without a terminal", - "0ea6a78efb": "Chat session" + "0ea6a78efb": "Chat session", + "startingCodexChat": "Starting Codex chat…" }, "RecentTabSwitcher": { "329638ff6f": "Switch Tab",