fix: queue AI Vault resume command in the configured Windows shell (#6571)
Resume-in-tab/drag-resume queued a cmd.exe-syntax command into a freshly spawned tab whose live shell is the configured Windows shell (default PowerShell). PowerShell mis-parsed the cmd ""-doubled wrapper and reported "is not recognized as an internal or external command, operable program or batch file", so the session never resumed. Resolve terminalWindowsShell to a startup-shell family and quote the queued command per shell (PowerShell Set-Location/$env, POSIX cd for git-bash), only emitting the cmd /d /s /c wrapper when the live shell is cmd. The copy-to-clipboard command is unchanged and stays cmd-wrapped. Fixes #6152 Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
396f9c342a
commit
bcce8e37e2
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { buildAiVaultResumeCommand } from '../../../shared/ai-vault-types'
|
||||
import {
|
||||
buildAiVaultResumeCommandForWorktree,
|
||||
buildAiVaultResumeStartupForWorktree,
|
||||
|
|
@ -27,6 +28,7 @@ type AiVaultResumeCommandState = Pick<
|
|||
function makeState(args: {
|
||||
worktreePath: string
|
||||
localWindowsRuntimePreference?: RuntimePreference
|
||||
terminalWindowsShell?: string
|
||||
}): AiVaultResumeCommandState {
|
||||
return {
|
||||
activeRepoId: 'repo-1',
|
||||
|
|
@ -45,6 +47,7 @@ function makeState(args: {
|
|||
],
|
||||
settings: {
|
||||
localWindowsRuntimeDefault: { kind: 'windows-host' },
|
||||
...(args.terminalWindowsShell ? { terminalWindowsShell: args.terminalWindowsShell } : {}),
|
||||
agentDefaultArgs: { claude: '', codex: '' },
|
||||
agentDefaultEnv: { claude: {}, codex: {} }
|
||||
},
|
||||
|
|
@ -61,9 +64,31 @@ function makeState(args: {
|
|||
}
|
||||
|
||||
describe('ai vault resume command runtime', () => {
|
||||
it('uses Windows command wrapping for Windows-host projects', () => {
|
||||
it('queues a PowerShell-valid command for the default Windows shell', () => {
|
||||
// Why: the queued command is typed into the live tab shell (default
|
||||
// PowerShell), which mis-parses the cmd `""`-doubled wrapper (#6152).
|
||||
const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' })
|
||||
|
||||
expect(
|
||||
buildAiVaultResumeCommandForWorktree({
|
||||
state,
|
||||
worktreeId: 'repo-1::worktree-1',
|
||||
session: {
|
||||
agent: 'claude',
|
||||
sessionId: 'session one',
|
||||
cwd: 'C:\\Users\\alice\\repo',
|
||||
codexHome: null
|
||||
}
|
||||
})
|
||||
).toBe("Set-Location -LiteralPath 'C:\\Users\\alice\\repo'; claude '--resume' 'session one'")
|
||||
})
|
||||
|
||||
it('keeps the cmd wrapper when the configured Windows shell is cmd.exe', () => {
|
||||
const state = makeState({
|
||||
worktreePath: 'C:\\Users\\alice\\repo',
|
||||
terminalWindowsShell: 'cmd.exe'
|
||||
})
|
||||
|
||||
expect(
|
||||
buildAiVaultResumeCommandForWorktree({
|
||||
state,
|
||||
|
|
@ -78,6 +103,40 @@ describe('ai vault resume command runtime', () => {
|
|||
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\alice\\repo"" && claude ""--resume"" ""session one"""')
|
||||
})
|
||||
|
||||
it('queues a POSIX command for the Git Bash Windows shell', () => {
|
||||
const state = makeState({
|
||||
worktreePath: 'C:\\Users\\alice\\repo',
|
||||
terminalWindowsShell: 'git-bash'
|
||||
})
|
||||
|
||||
expect(
|
||||
buildAiVaultResumeCommandForWorktree({
|
||||
state,
|
||||
worktreeId: 'repo-1::worktree-1',
|
||||
session: {
|
||||
agent: 'claude',
|
||||
sessionId: 'session one',
|
||||
cwd: 'C:\\Users\\alice\\repo',
|
||||
codexHome: null
|
||||
}
|
||||
})
|
||||
).toBe("cd 'C:\\Users\\alice\\repo' && claude '--resume' 'session one'")
|
||||
})
|
||||
|
||||
it('keeps the cmd wrapper for the copy-to-clipboard command on Windows', () => {
|
||||
// Regression guard: the copy path is self-contained for pasting into cmd.exe
|
||||
// and must stay cmd-wrapped even though the queued path now follows the shell.
|
||||
expect(
|
||||
buildAiVaultResumeCommand({
|
||||
agent: 'claude',
|
||||
sessionId: 'session one',
|
||||
cwd: 'C:\\Users\\alice\\repo',
|
||||
platform: 'win32',
|
||||
codexHome: null
|
||||
})
|
||||
).toBe('cmd /d /s /c "cd /d ""C:\\Users\\alice\\repo"" && claude --resume ""session one"""')
|
||||
})
|
||||
|
||||
it('uses configured agent defaults for resumable session history entries', () => {
|
||||
const state = makeState({
|
||||
worktreePath: 'C:\\Users\\alice\\repo',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
resolveTuiAgentLaunchEnv
|
||||
} from '../../../shared/tui-agent-launch-defaults'
|
||||
import { parseWslUncPath } from '../../../shared/wsl-paths'
|
||||
import { resolveWindowsShellStartupFamily } from '../../../shared/windows-terminal-shell'
|
||||
import type { AgentStartupShell } from '../../../shared/tui-agent-startup-shell'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
|
||||
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
|
||||
|
|
@ -65,6 +67,14 @@ export function buildAiVaultResumeStartupForWorktree(args: {
|
|||
}): AiVaultResumeStartup {
|
||||
const platform = getAiVaultResumePlatform(args.state, args.worktreeId)
|
||||
const codexHome = getAiVaultResumeCodexHome(args.session.codexHome, platform)
|
||||
// Why: the queued command is typed verbatim into the freshly spawned tab whose
|
||||
// live shell is the configured Windows shell (default PowerShell). Hardcoding
|
||||
// cmd quoting made PowerShell mis-parse the `""`-doubled wrapper (#6152), so
|
||||
// resolve the actual shell to quote per-shell instead.
|
||||
const queuedShell: AgentStartupShell | undefined =
|
||||
platform === 'win32'
|
||||
? resolveWindowsShellStartupFamily(args.state.settings?.terminalWindowsShell)
|
||||
: undefined
|
||||
if (isResumableTuiAgent(args.session.agent)) {
|
||||
const startupPlan = buildAgentResumeStartupPlan({
|
||||
agent: args.session.agent,
|
||||
|
|
@ -74,9 +84,7 @@ export function buildAiVaultResumeStartupForWorktree(args: {
|
|||
...(args.commandOverride?.trim() ? { [args.session.agent]: args.commandOverride } : {})
|
||||
},
|
||||
platform,
|
||||
// Why: copied AI Vault commands are shell-wrapped for portability; the
|
||||
// same inner command must be queued so drag/click resume match copy.
|
||||
shell: platform === 'win32' ? 'cmd' : undefined,
|
||||
shell: queuedShell,
|
||||
agentArgs: resolveTuiAgentLaunchArgs(
|
||||
args.session.agent,
|
||||
args.state.settings?.agentDefaultArgs
|
||||
|
|
@ -89,7 +97,8 @@ export function buildAiVaultResumeStartupForWorktree(args: {
|
|||
resumeCommand: startupPlan.launchCommand,
|
||||
cwd: args.session.cwd,
|
||||
platform,
|
||||
codexHome
|
||||
codexHome,
|
||||
shell: queuedShell
|
||||
}),
|
||||
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
||||
launchConfig: startupPlan.launchConfig
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import {
|
||||
commandSeparator,
|
||||
quoteStartupArg,
|
||||
type AgentStartupShell
|
||||
} from './tui-agent-startup-shell'
|
||||
import type { TuiAgent } from './types'
|
||||
|
||||
export const AI_VAULT_AGENTS = [
|
||||
|
|
@ -106,8 +111,26 @@ export function buildAiVaultResumeShellCommand(args: {
|
|||
cwd: string | null
|
||||
platform: NodeJS.Platform
|
||||
codexHome?: string | null
|
||||
// Why: the QUEUED resume command is typed into the live tab shell, so its
|
||||
// cd/env prefix must match that shell. The copy-to-clipboard string omits this
|
||||
// and keeps the self-contained `cmd /d /s /c` wrapper (its documented purpose).
|
||||
shell?: AgentStartupShell
|
||||
}): string {
|
||||
const { cwd, platform, codexHome } = args
|
||||
const { cwd, platform, codexHome, shell } = args
|
||||
|
||||
// Why: on Windows the queued command must target the configured live shell
|
||||
// (default PowerShell). PowerShell mis-parses the cmd `""`-doubled wrapper and
|
||||
// reports "operable program or batch file", so only re-wrap with cmd when the
|
||||
// live shell actually is cmd (or when no shell is given, i.e. the copy path).
|
||||
if (platform === 'win32' && shell && shell !== 'cmd') {
|
||||
return buildResumeShellCommandForShell({
|
||||
resumeCommand: args.resumeCommand,
|
||||
cwd,
|
||||
codexHome: codexHome?.trim() || null,
|
||||
shell
|
||||
})
|
||||
}
|
||||
|
||||
const resumeCommand = `${codexHomeEnvPrefix(codexHome?.trim() || null, platform)}${
|
||||
args.resumeCommand
|
||||
}`
|
||||
|
|
@ -123,6 +146,33 @@ export function buildAiVaultResumeShellCommand(args: {
|
|||
return `cd ${quoteShellArg(cwd, platform)} && ${resumeCommand}`
|
||||
}
|
||||
|
||||
function buildResumeShellCommandForShell(args: {
|
||||
resumeCommand: string
|
||||
cwd: string | null
|
||||
codexHome: string | null
|
||||
shell: Exclude<AgentStartupShell, 'cmd'>
|
||||
}): string {
|
||||
const { cwd, codexHome, shell } = args
|
||||
if (shell === 'posix') {
|
||||
// Why: git-bash on a Windows host runs a POSIX shell, so reuse the same
|
||||
// inline-env + `cd '<cwd>'` prefix as the non-Windows path.
|
||||
const envPrefix = codexHome ? `CODEX_HOME=${quoteStartupArg(codexHome, shell)} ` : ''
|
||||
const command = `${envPrefix}${args.resumeCommand}`
|
||||
return cwd ? `cd ${quoteStartupArg(cwd, shell)} && ${command}` : command
|
||||
}
|
||||
|
||||
const separator = commandSeparator(shell)
|
||||
const segments: string[] = []
|
||||
if (cwd) {
|
||||
segments.push(`Set-Location -LiteralPath ${quoteStartupArg(cwd, shell)}`)
|
||||
}
|
||||
if (codexHome) {
|
||||
segments.push(`$env:CODEX_HOME=${quoteStartupArg(codexHome, shell)}`)
|
||||
}
|
||||
segments.push(args.resumeCommand)
|
||||
return segments.join(separator)
|
||||
}
|
||||
|
||||
export function aiVaultAgentLabel(agent: AiVaultAgent): string {
|
||||
return AI_VAULT_AGENT_LABELS[agent]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveWindowsShellStartupFamily } from './windows-terminal-shell'
|
||||
|
||||
describe('resolveWindowsShellStartupFamily', () => {
|
||||
it('defaults to PowerShell when unset', () => {
|
||||
expect(resolveWindowsShellStartupFamily(undefined)).toBe('powershell')
|
||||
expect(resolveWindowsShellStartupFamily(null)).toBe('powershell')
|
||||
expect(resolveWindowsShellStartupFamily(' ')).toBe('powershell')
|
||||
})
|
||||
|
||||
it('treats PowerShell and pwsh as PowerShell', () => {
|
||||
expect(resolveWindowsShellStartupFamily('powershell.exe')).toBe('powershell')
|
||||
expect(resolveWindowsShellStartupFamily('pwsh.exe')).toBe('powershell')
|
||||
expect(resolveWindowsShellStartupFamily('C:\\Program Files\\PowerShell\\7\\pwsh.exe')).toBe(
|
||||
'powershell'
|
||||
)
|
||||
})
|
||||
|
||||
it('maps cmd.exe to cmd quoting', () => {
|
||||
expect(resolveWindowsShellStartupFamily('cmd.exe')).toBe('cmd')
|
||||
expect(resolveWindowsShellStartupFamily('C:\\Windows\\System32\\cmd.exe')).toBe('cmd')
|
||||
})
|
||||
|
||||
it('maps Git Bash and WSL shells to POSIX quoting', () => {
|
||||
expect(resolveWindowsShellStartupFamily('git-bash')).toBe('posix')
|
||||
expect(resolveWindowsShellStartupFamily('wsl.exe')).toBe('posix')
|
||||
expect(resolveWindowsShellStartupFamily('C:\\Program Files\\Git\\bin\\bash.exe')).toBe('posix')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import type { AgentStartupShell } from './tui-agent-startup-shell'
|
||||
|
||||
export const WINDOWS_GIT_BASH_SHELL = 'git-bash'
|
||||
|
||||
export type BuiltInWindowsTerminalShell =
|
||||
|
|
@ -5,3 +7,31 @@ export type BuiltInWindowsTerminalShell =
|
|||
| '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.
|
||||
if (basename === 'wsl.exe' || basename === 'wsl' || basename === 'bash.exe') {
|
||||
return 'posix'
|
||||
}
|
||||
return 'powershell'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue