Respect terminalWindowsShell setting for local Windows agent launches (#7526)

Ensure agent CLI startup and draft launch commands use the correct quoting
format based on the user's configured local Windows shell (e.g., cmd.exe).
This avoids using host settings for remote/SSH targets where local shell
preferences do not apply.
This commit is contained in:
Jinjing
2026-07-06 01:05:56 -07:00
committed by GitHub
parent be4d963905
commit 2b5f6af0e9
5 changed files with 295 additions and 32 deletions
+136
View File
@@ -3538,6 +3538,7 @@ describe('OrcaRuntimeService', () => {
...store,
getSettings: () => ({
...store.getSettings(),
terminalWindowsShell: 'cmd.exe',
agentCmdOverrides: {}
}),
getRepos: () => [remoteRepo],
@@ -7264,6 +7265,94 @@ describe('OrcaRuntimeService', () => {
)
})
it('quotes local Windows bare agent command defaults for cmd.exe terminal creates', async () => {
setPlatform('win32')
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
...store,
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: [],
terminalWindowsShell: 'cmd.exe',
agentCmdOverrides: {},
agentDefaultArgs: { claude: '--dangerously-skip-permissions' },
agentDefaultEnv: {}
})
}
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
command: 'claude',
title: 'worker'
})
const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined
expect(spawnCall?.command).toBe('claude "--dangerously-skip-permissions"')
})
it('does not use the local Windows shell setting for remote Windows bare agent creates', async () => {
const remoteRepo = {
id: TEST_REPO_ID,
path: 'C:/remote/repo',
displayName: 'repo',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-1'
}
const remoteStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === TEST_REPO_ID ? remoteRepo : undefined),
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: [],
terminalWindowsShell: 'cmd.exe',
agentCmdOverrides: {},
agentDefaultArgs: { claude: '--dangerously-skip-permissions' },
agentDefaultEnv: {}
})
}
const provider = {
exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
listWorktrees: vi.fn().mockResolvedValue([
{
path: 'C:/remote/repo',
head: 'abc',
branch: 'main',
isBare: false,
isMainWorktree: true
}
])
}
const spawn = vi.fn().mockResolvedValue({ id: 'pty-remote-windows-bare' })
registerSshGitProvider('ssh-1', provider as never)
const runtime = new OrcaRuntimeService(remoteStore as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
try {
await runtime.createTerminal('path:C:/remote/repo', {
command: 'claude',
title: 'worker'
})
const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined
expect(spawnCall?.command).toBe("claude '--dangerously-skip-permissions'")
} finally {
unregisterSshGitProvider('ssh-1')
}
})
it('matches canonical bare agent commands when a command override is configured', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
@@ -17184,6 +17273,53 @@ describe('OrcaRuntimeService', () => {
})
})
it('uses cmd.exe quoting for mobile agent launch commands in local Windows host runtimes', async () => {
await withPlatform('win32', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent-cmd' })
const runtime = new OrcaRuntimeService({
...store,
getProjects: () => [
{
id: 'project-1',
displayName: 'repo',
badgeColor: 'blue',
sourceRepoIds: [TEST_REPO_ID],
localWindowsRuntimePreference: { kind: 'windows-host' },
createdAt: 0,
updatedAt: 0
}
],
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: [],
agentCmdOverrides: { 'command-code': 'command-code --profile mobile' },
agentDefaultArgs: { 'command-code': '--note "can\'t"' },
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' },
terminalWindowsShell: 'cmd.exe'
})
} as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
agent: 'command-code'
})
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({
command: 'command-code --profile mobile "--note" "can\'t"',
cwd: TEST_WORKTREE_PATH,
worktreeId: TEST_WORKTREE_ID
})
)
})
})
it('publishes headless mobile session agent identity with synthesized PTY status', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' })
const runtime = new OrcaRuntimeService({
+29 -1
View File
@@ -188,6 +188,7 @@ import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../shared/tui-agent-launch-defaults'
import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell'
import {
getTuiAgentLaunchCommand,
isTuiAgent,
@@ -787,6 +788,7 @@ type RuntimeStore = {
agentCmdOverrides?: GlobalSettings['agentCmdOverrides']
agentDefaultArgs?: GlobalSettings['agentDefaultArgs']
agentDefaultEnv?: GlobalSettings['agentDefaultEnv']
terminalWindowsShell?: GlobalSettings['terminalWindowsShell']
agentStatusHooksEnabled?: GlobalSettings['agentStatusHooksEnabled']
defaultTaskSource?: GlobalSettings['defaultTaskSource']
defaultTaskViewPreset?: GlobalSettings['defaultTaskViewPreset']
@@ -12353,6 +12355,11 @@ export class OrcaRuntimeService {
// Linux over SSH. Startup command quoting must target the shell that runs it.
const agentLaunchPlatform = this.getAgentLaunchPlatformForRepo(repo)
const isRemote = repoIsRemote(repo)
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform: agentLaunchPlatform,
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const draftLaunchPlan = buildAgentDraftLaunchPlan({
agent,
draft: content,
@@ -12360,6 +12367,7 @@ export class OrcaRuntimeService {
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
platform: agentLaunchPlatform,
shell: queuedShell,
isRemote
})
if (draftLaunchPlan) {
@@ -12383,6 +12391,7 @@ export class OrcaRuntimeService {
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
platform: agentLaunchPlatform,
shell: queuedShell,
isRemote,
allowEmptyPromptLaunch: true
})
@@ -12418,6 +12427,12 @@ export class OrcaRuntimeService {
// Why: CLI clients may target SSH runtimes from macOS/Windows, so quote for
// the workspace shell rather than the client shell.
const agentLaunchPlatform = this.getAgentLaunchPlatformForRepo(repo)
const isRemote = repoIsRemote(repo)
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform: agentLaunchPlatform,
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const startupPlan = buildAgentStartupPlan({
agent,
prompt: prompt ?? '',
@@ -12425,7 +12440,8 @@ export class OrcaRuntimeService {
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
platform: agentLaunchPlatform,
isRemote: repoIsRemote(repo),
shell: queuedShell,
isRemote,
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
@@ -15632,6 +15648,11 @@ export class OrcaRuntimeService {
const settings = this.store.getSettings()
const platform = this.getAgentLaunchPlatformForWorkspace(workspace)
const isRemote = repoIsRemote(workspace.repo)
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform,
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const agent = resolveBareAgentLaunchCommand({
command: opts.command,
settings,
@@ -15649,6 +15670,7 @@ export class OrcaRuntimeService {
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
platform,
shell: queuedShell,
isRemote,
allowEmptyPromptLaunch: true
})
@@ -16193,6 +16215,11 @@ export class OrcaRuntimeService {
// Why: an SSH workspace runs the CLI through the relay shim (plain `orca`),
// so the Linux-only `orca-ide` rename must not be applied.
const isRemote = workspace.repo ? repoIsRemote(workspace.repo) : repoIsRemote(workspace)
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform,
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const startupPlan = buildAgentStartupPlan({
agent: opts.agent,
prompt: '',
@@ -16200,6 +16227,7 @@ export class OrcaRuntimeService {
agentArgs: resolveTuiAgentLaunchArgs(opts.agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(opts.agent, settings.agentDefaultEnv),
platform,
shell: queuedShell,
isRemote,
allowEmptyPromptLaunch: true
})
@@ -25,6 +25,7 @@ const store = {
agentDefaultArgs: Record<string, string>
agentDefaultEnv: Record<string, Record<string, string>>
activeRuntimeEnvironmentId: string | null
terminalWindowsShell?: string
experimentalNativeChat?: boolean
openAgentTabsInChatByDefault?: boolean
},
@@ -409,7 +410,102 @@ describe('launchAgentInNewTab', () => {
)
})
it('quotes local Windows default agent args for cmd.exe empty launches', async () => {
store.settings.terminalWindowsShell = 'cmd.exe'
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'claude',
worktreeId: 'wt-1',
launchPlatform: 'win32'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: 'claude "--dangerously-skip-permissions"'
})
)
})
it('keeps PowerShell quoting for local Windows default agent args', async () => {
store.settings.terminalWindowsShell = 'powershell.exe'
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'claude',
worktreeId: 'wt-1',
launchPlatform: 'win32'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: "claude '--dangerously-skip-permissions'"
})
)
})
it('quotes local Windows explicit agent args for cmd.exe prompt launches', async () => {
store.settings.terminalWindowsShell = 'cmd.exe'
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'codex',
worktreeId: 'wt-1',
prompt: 'fix the spinner',
agentArgs: '--model gpt-5',
launchPlatform: 'win32'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: 'codex "--model" "gpt-5" "fix the spinner"'
})
)
})
it('quotes local Windows draft launches for Git Bash', async () => {
store.settings.terminalWindowsShell = 'git-bash'
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'claude',
worktreeId: 'wt-1',
prompt: "review Bob's change",
promptDelivery: 'draft',
launchPlatform: 'win32'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: "claude '--dangerously-skip-permissions' --prefill 'review Bob'\\''s change'"
})
)
})
it('does not use the local Windows shell setting for remote Windows launches', async () => {
store.settings.terminalWindowsShell = 'cmd.exe'
store.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: 'C:\\remote\\repo' }]
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'claude',
worktreeId: 'wt-1'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: "claude '--dangerously-skip-permissions'"
})
)
})
it('uses WSL launch quoting by default for Windows-path projects forced to WSL', async () => {
store.settings.terminalWindowsShell = 'cmd.exe'
store.projects = [
{
id: 'repo-1',
+21 -31
View File
@@ -22,6 +22,7 @@ import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../shared/tui-agent-launch-defaults'
import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell'
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
import { repoIsRemote } from '../../../shared/agent-launch-remote'
import { seedCommandCodeSubmittedPromptStatus } from '@/lib/command-code-prompt-status-seed'
@@ -119,12 +120,26 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
// Why: SSH remotes deploy the CLI shim as plain `orca`, so the Linux-only
// `orca-ide` rename must not be applied for remote launches.
const isRemote = repo ? repoIsRemote(repo) : false
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform: resolvedLaunchPlatform,
isRemote,
terminalWindowsShell: store.settings?.terminalWindowsShell
})
const cmdOverrides = store.settings?.agentCmdOverrides ?? {}
const effectiveAgentArgs =
agentArgs !== undefined
? agentArgs
: resolveTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs)
const agentEnv = resolveTuiAgentLaunchEnv(agent, store.settings?.agentDefaultEnv)
const startupPlanBase = {
agent,
cmdOverrides,
platform: resolvedLaunchPlatform,
shell: queuedShell,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv
}
const trimmedPrompt = prompt?.trim() ?? ''
const hasPrompt = trimmedPrompt.length > 0
const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start'
@@ -143,13 +158,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
// Why: generated multi-line prompts are too large to echo through a shell
// argv/prefill command. Launch cleanly, then paste+submit inside the TUI.
startupPlan = buildAgentStartupPlan({
agent,
...startupPlanBase,
prompt: '',
cmdOverrides,
platform: resolvedLaunchPlatform,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv,
allowEmptyPromptLaunch: true
})
pasteDraftAfterLaunch = trimmedPrompt
@@ -157,13 +167,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
forcePasteAfterLaunch = true
} else if (hasPrompt && promptDelivery === 'draft') {
const draftLaunchPlan = buildAgentDraftLaunchPlan({
agent,
draft: trimmedPrompt,
cmdOverrides,
platform: resolvedLaunchPlatform,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv
...startupPlanBase,
draft: trimmedPrompt
})
if (draftLaunchPlan) {
startupPlan = {
@@ -179,38 +184,23 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
}
} else {
startupPlan = buildAgentStartupPlan({
agent,
...startupPlanBase,
prompt: '',
cmdOverrides,
platform: resolvedLaunchPlatform,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv,
allowEmptyPromptLaunch: true
})
pasteDraftAfterLaunch = trimmedPrompt
}
} else if (hasPrompt && isFollowupPath) {
startupPlan = buildAgentStartupPlan({
agent,
...startupPlanBase,
prompt: '',
cmdOverrides,
platform: resolvedLaunchPlatform,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv,
allowEmptyPromptLaunch: true
})
pasteDraftAfterLaunch = trimmedPrompt
} else {
startupPlan = buildAgentStartupPlan({
agent,
...startupPlanBase,
prompt: hasPrompt ? trimmedPrompt : '',
cmdOverrides,
platform: resolvedLaunchPlatform,
isRemote,
agentArgs: effectiveAgentArgs,
agentEnv,
allowEmptyPromptLaunch: !hasPrompt
})
}
+13
View File
@@ -35,3 +35,16 @@ export function resolveWindowsShellStartupFamily(
}
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
}
return resolveWindowsShellStartupFamily(args.terminalWindowsShell)
}