Files
orca/src/shared/windows-terminal-shell.ts
T
m4air 474b75c8da fix(terminal): quote agent resume for cmd.exe when the Windows shell setting is unset
A cold restore right after restart can run before the renderer store hydrates
`settings`, so terminalWindowsShell is momentarily undefined. The renderer
predicted PowerShell quoting for that empty case, but the spawn side launches
%COMSPEC% (cmd.exe) when no shell is configured (local-pty-launch-plan.ts /
local-pty-session-operations.ts). The single-quoted resume argv then reached the
cmd.exe pane literally: codex rejected `'resume'` ("unrecognized subcommand
''resume''") and claude read `'--dangerously-skip-permissions'` as a prompt,
starting a new session instead of resuming (#12320 residual).

resolveLocalWindowsAgentStartupShell now mirrors that %COMSPEC% fallback, quoting
an unset local Windows shell for cmd. Explicit powershell.exe/cmd.exe/git-bash/
wsl.exe settings, SSH/remote targets, and WSL panes are unchanged.
2026-09-07 03:24:46 -07:00

68 lines
2.5 KiB
TypeScript

import type { AgentStartupShell } from './tui-agent-startup-shell'
export const WINDOWS_GIT_BASH_SHELL = 'git-bash'
export type BuiltInWindowsTerminalShell =
| 'powershell.exe'
| 'cmd.exe'
| 'wsl.exe'
| typeof WINDOWS_GIT_BASH_SHELL
/**
* Classifies a configured `terminalWindowsShell` value into the startup-shell
* family used to quote queued commands. Git Bash / wsl.exe run a POSIX shell;
* cmd.exe needs cmd quoting; everything else (PowerShell, pwsh, unknown) is
* treated as PowerShell, matching the Windows default.
*/
export function resolveWindowsShellStartupFamily(
shell: string | null | undefined
): AgentStartupShell {
const trimmed = shell?.trim()
if (!trimmed) {
return 'powershell'
}
if (trimmed === WINDOWS_GIT_BASH_SHELL) {
return 'posix'
}
const basename = trimmed.replaceAll('\\', '/').split('/').pop()?.toLowerCase() ?? ''
if (basename === 'cmd.exe') {
return 'cmd'
}
// Why: wsl.exe and bash.exe (Git for Windows) launch POSIX shells, so queued
// commands must use POSIX quoting and `cd '<cwd>'` rather than cmd/PowerShell.
// Extension-less forms reach the same executables through PATHEXT.
if (
basename === 'wsl.exe' ||
basename === 'wsl' ||
basename === 'bash.exe' ||
basename === 'bash'
) {
return 'posix'
}
return 'powershell'
}
export function resolveLocalWindowsAgentStartupShell(args: {
platform: NodeJS.Platform
isRemote: boolean
terminalWindowsShell?: string | null
}): AgentStartupShell | undefined {
// Why: terminalWindowsShell describes the local host shell; SSH/remote
// targets need their own shell signal before we can safely override quoting.
if (args.platform !== 'win32' || args.isRemote) {
return undefined
}
// Why cmd, not the PowerShell default: with no configured shell the spawn side
// falls back to %COMSPEC% (cmd.exe) — local-pty-launch-plan.ts and
// local-pty-session-operations.ts both `... || process.env.COMSPEC || 'powershell.exe'`.
// A cold restore right after restart can run before the renderer store hydrates
// `settings`, so terminalWindowsShell is momentarily empty; predicting PowerShell
// then quotes the resume argv in single quotes that cmd.exe passes through
// literally, breaking the resume (#12320 residual: codex "unrecognized subcommand
// ''resume''", claude reads '--dangerously-skip-permissions' as a prompt).
if (!args.terminalWindowsShell?.trim()) {
return 'cmd'
}
return resolveWindowsShellStartupFamily(args.terminalWindowsShell)
}