Fix Windows AI Vault resumes using the wrong shell (#13420)

* fix: use project shell for AI Vault resumes

* fix: preserve WSL cwd fallback semantics

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin 2026-08-09 19:52:16 -07:00 committed by GitHub
parent ce8c3267b7
commit bdd763188f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 658 additions and 109 deletions

View File

@ -16,6 +16,7 @@ import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-a
import { AGENT_SESSION_CLAIM_DIGEST_VERSION } from '../../shared/agent-session-host-authority'
import { PtyWriteUnavailableError } from '../providers/pty-write-unavailable-error'
import { TerminalSessionOwnerUnverifiedError } from '../daemon/daemon-errors'
import type * as Wsl from '../wsl'
const isWindowsHost = process.platform === 'win32'
const posixOnlyIt = isWindowsHost ? it.skip : it
@ -57,6 +58,7 @@ const {
piBuildPtyEnvMock,
piClearPtyMock,
isPwshAvailableMock,
wslUncDirectoryExistsAsyncMock,
trackMock,
classifyErrorMock,
registerPtyMock,
@ -86,6 +88,7 @@ const {
openCodeBuildPtyEnvMock: vi.fn(),
mimoCodeBuildPtyEnvMock: vi.fn(),
isPwshAvailableMock: vi.fn(),
wslUncDirectoryExistsAsyncMock: vi.fn(),
openCodeClearPtyMock: vi.fn(),
buildAgentHookEnvMock: vi.fn(),
clearAgentHookPaneStateMock: vi.fn(),
@ -183,6 +186,11 @@ vi.mock('../pwsh', () => ({
isPwshAvailableAsync: isPwshAvailableMock
}))
vi.mock('../wsl', async (importOriginal) => ({
...(await importOriginal<typeof Wsl>()),
wslUncDirectoryExistsAsync: (...args: unknown[]) => wslUncDirectoryExistsAsyncMock(...args)
}))
vi.mock('../telemetry/client', () => ({
track: trackMock
}))
@ -384,6 +392,8 @@ describe('registerPtyHandlers', () => {
piBuildPtyEnvMock.mockReset()
piClearPtyMock.mockReset()
isPwshAvailableMock.mockReset()
wslUncDirectoryExistsAsyncMock.mockReset()
wslUncDirectoryExistsAsyncMock.mockResolvedValue(null)
trackMock.mockReset()
classifyErrorMock.mockReset()
registerPtyMock.mockReset()
@ -12125,6 +12135,272 @@ describe('registerPtyHandlers', () => {
expect(result.startupCwdFallback).toEqual({ kind: 'worktree', cwd: worktreePath })
})
it('keeps an existing POSIX startup cwd for the selected WSL runtime', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-cwd' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
wslUncDirectoryExistsAsyncMock.mockResolvedValue(true)
statSyncMock.mockImplementation((target: string) => {
if (target === '/home/alice/repo') {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
})
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/repo',
cwdFallback: 'worktree',
worktreeId: 'repo-1::C:\\Users\\alice\\repo',
projectRuntime: {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'repo-1',
distro: 'Ubuntu-24.04',
reason: 'project-override',
cacheKey: 'repo-1:wsl'
}
}
})
expect(statSyncMock).not.toHaveBeenCalledWith('/home/alice/repo')
expect(wslUncDirectoryExistsAsyncMock).toHaveBeenCalledWith(
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\alice\\repo'
)
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: '/home/alice/repo', shellOverride: 'wsl.exe' })
)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('falls back when the selected WSL runtime reports a POSIX cwd missing', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-missing-cwd' })
const worktreePath = 'C:/Users/alice/repo'
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
wslUncDirectoryExistsAsyncMock.mockResolvedValue(false)
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/deleted',
cwdFallback: 'worktree',
worktreeId: `repo-1::${worktreePath}`,
projectRuntime: {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'repo-1',
distro: 'Ubuntu-24.04',
reason: 'project-override',
cacheKey: 'repo-1:wsl'
}
}
})) as { startupCwdFallback?: { kind: string; cwd: string } }
expect(providerSpawn).toHaveBeenCalledWith(expect.objectContaining({ cwd: worktreePath }))
expect(result.startupCwdFallback).toEqual({ kind: 'worktree', cwd: worktreePath })
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('preserves a POSIX WSL cwd when the distro probe is inconclusive', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-inconclusive-cwd' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
wslUncDirectoryExistsAsyncMock.mockResolvedValue(null)
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/repo',
cwdFallback: 'worktree',
worktreeId: 'repo-1::C:/Users/alice/repo',
shellOverride: 'wsl.exe'
})) as { startupCwdFallback?: unknown }
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: '/home/alice/repo' })
)
expect(result.startupCwdFallback).toBeUndefined()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('preserves a POSIX cwd when WSL owns it but no distro can be resolved', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-no-distro-cwd' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
_setWslCachesForTests({ available: true, distros: [] })
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/repo',
cwdFallback: 'worktree',
worktreeId: 'repo-1::C:/Users/alice/repo',
shellOverride: 'wsl.exe'
})) as { startupCwdFallback?: unknown }
expect(statSyncMock).not.toHaveBeenCalledWith('/home/alice/repo')
expect(wslUncDirectoryExistsAsyncMock).not.toHaveBeenCalled()
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: '/home/alice/repo' })
)
expect(result.startupCwdFallback).toBeUndefined()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it.each([
{ exists: true, expectedCwd: '/home/alice/repo', expectedFallback: undefined },
{
exists: false,
expectedCwd: '\\\\wsl.localhost\\Ubuntu\\home\\alice',
expectedFallback: {
kind: 'worktree',
cwd: '\\\\wsl.localhost\\Ubuntu\\home\\alice'
}
}
])('resolves POSIX cwd existence for a WSL UNC workspace ($exists)', async (testCase) => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-unc-cwd' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
wslUncDirectoryExistsAsyncMock.mockResolvedValueOnce(testCase.exists)
if (!testCase.exists) {
wslUncDirectoryExistsAsyncMock.mockResolvedValueOnce(true)
}
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/repo',
cwdFallback: 'worktree',
worktreeId: 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\alice'
})) as { startupCwdFallback?: { kind: string; cwd: string } }
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: testCase.expectedCwd })
)
expect(result.startupCwdFallback).toEqual(testCase.expectedFallback)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('preserves a missing POSIX cwd when its WSL workspace root is also missing', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-missing-root' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
wslUncDirectoryExistsAsyncMock.mockResolvedValue(false)
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/home/alice/deleted',
cwdFallback: 'worktree',
worktreeId: 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\alice'
})) as { startupCwdFallback?: unknown }
expect(wslUncDirectoryExistsAsyncMock).toHaveBeenCalledTimes(2)
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: '/home/alice/deleted' })
)
expect(result.startupCwdFallback).toBeUndefined()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('validates a /mnt drive cwd through its native Windows path', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-mnt-cwd' })
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/mnt/c/Users/alice/repo',
cwdFallback: 'worktree',
worktreeId: 'repo-1::C:/Users/alice/repo',
shellOverride: 'wsl.exe'
})
expect(statSyncMock).toHaveBeenCalledWith('C:\\Users\\alice\\repo')
expect(wslUncDirectoryExistsAsyncMock).not.toHaveBeenCalled()
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: '/mnt/c/Users/alice/repo' })
)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('still falls back a missing Windows cwd when the selected runtime is WSL', async () => {
const originalPlatform = process.platform
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-wsl-windows-cwd' })
const worktreePath = 'C:/Users/alice/repo'
const missingCwd = `${worktreePath}/deleted-folder`
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
statSyncMock.mockImplementation((target: string) => {
if (target === missingCwd) {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
})
try {
installDaemonTestProvider({ spawn: providerSpawn })
registerPtyHandlers(mainWindow as never)
const result = (await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: missingCwd,
cwdFallback: 'worktree',
worktreeId: `repo-1::${worktreePath}`,
shellOverride: 'wsl.exe'
})) as { startupCwdFallback?: { kind: string; cwd: string } }
expect(providerSpawn).toHaveBeenCalledWith(
expect.objectContaining({ cwd: worktreePath, shellOverride: 'wsl.exe' })
)
expect(result.startupCwdFallback).toEqual({ kind: 'worktree', cwd: worktreePath })
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('keeps a missing cwd unchanged without the fallback flag', async () => {
registerPtyHandlers(mainWindow as never)
existsSyncMock.mockImplementation((target: string) => target !== '/repo/app/deleted-folder')

View File

@ -139,7 +139,7 @@ import {
resolveTerminalStartupCwdForWorkspace,
type TerminalStartupCwdMissingDirFallback
} from '../../shared/terminal-startup-cwd'
import { isWslUncPath } from '../../shared/wsl-paths'
import { isWslUncPath, toWindowsWslPath } from '../../shared/wsl-paths'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority'
import {
@ -150,7 +150,7 @@ import {
clearMigrationUnsupportedPty,
clearMigrationUnsupportedPtysForPaneKey
} from '../agent-hooks/migration-unsupported-pty-state'
import { parseWslPath } from '../wsl'
import { parseWslPath, wslUncDirectoryExistsAsync } from '../wsl'
import { mergePersistedWindowsPath, resolvePathEnvKey } from '../pty/windows-environment-path'
import { addOrcaWslInteropEnv, stampWslOrchestrationCompatibilityHost } from '../pty/wsl-orca-env'
import { PtyProducerFlowController } from './pty-producer-flow-control'
@ -5775,15 +5775,73 @@ export function registerPtyHandlers(
await startupPromise
}
// Why: honor the fallback only for fresh local spawns — reattach needs exact cwd and SSH can't probe the local filesystem.
const allowMissingCwdFallback =
const requestedMissingCwdFallback =
!args.connectionId && !args.sessionId && args.cwdFallback === 'worktree'
const isWslOwnedPosixCwd =
args.cwd?.startsWith('/') === true && !/^\/[A-Za-z](?:\/|$)/.test(args.cwd)
const startupWorkspaceCwd =
requestedMissingCwdFallback && isWslOwnedPosixCwd
? resolvePtySpawnStartupCwd(args.worktreeId, '.')
: undefined
const initiallyResolvedStartupCwd =
requestedMissingCwdFallback && isWslOwnedPosixCwd
? resolvePtySpawnStartupCwd(args.worktreeId, args.cwd)
: undefined
const startupTerminalRuntimeOptions =
requestedMissingCwdFallback && process.platform === 'win32'
? resolveLocalWindowsTerminalRuntimeOptions({
requestedShellOverride: args.shellOverride,
settings: getSettings?.(),
projectRuntime: args.projectRuntime,
fallbackHostShell: process.env.COMSPEC || 'powershell.exe'
})
: undefined
const wslRuntimeOwnsStartupCwd =
requestedMissingCwdFallback &&
isWslOwnedPosixCwd &&
(isWslShellName(startupTerminalRuntimeOptions?.shellOverride) ||
isWslUncPath(startupWorkspaceCwd ?? ''))
const startupWslContext = wslRuntimeOwnsStartupCwd
? resolveWslSessionContext({
cwd: startupWorkspaceCwd,
shellOverride: startupTerminalRuntimeOptions?.shellOverride,
terminalWindowsWslDistro: startupTerminalRuntimeOptions?.terminalWindowsWslDistro
})
: undefined
let wslStartupCwdExists: boolean | null = null
let wslWorkspaceCwdExists: boolean | null = null
if (startupWslContext && initiallyResolvedStartupCwd) {
const validationCwd = toWindowsWslPath(
initiallyResolvedStartupCwd,
startupWslContext.distro
)
wslStartupCwdExists = isWslUncPath(validationCwd)
? await wslUncDirectoryExistsAsync(validationCwd)
: localStartupCwdDirectoryExists(validationCwd)
if (wslStartupCwdExists === false && startupWorkspaceCwd) {
wslWorkspaceCwdExists = isWslUncPath(startupWorkspaceCwd)
? await wslUncDirectoryExistsAsync(startupWorkspaceCwd)
: localStartupCwdDirectoryExists(startupWorkspaceCwd)
}
}
const allowMissingCwdFallback =
requestedMissingCwdFallback &&
(!wslRuntimeOwnsStartupCwd ||
(wslStartupCwdExists === false && wslWorkspaceCwdExists === true))
let didFallbackToWorkspaceRootCwd = false
const cwd = resolvePtySpawnStartupCwd(
args.worktreeId,
args.cwd,
allowMissingCwdFallback
? {
directoryExists: localStartupCwdDirectoryExists,
directoryExists: (path) =>
startupWslContext &&
wslStartupCwdExists === false &&
path === initiallyResolvedStartupCwd
? false
: startupWslContext && path === startupWorkspaceCwd
? wslWorkspaceCwdExists === true
: localStartupCwdDirectoryExists(path),
onFallbackToWorkspaceRoot: () => {
didFallbackToWorkspaceRootCwd = true
}

View File

@ -978,6 +978,31 @@ describe('LocalPtyProvider', () => {
expect(spawnCall[2].env.HISTFILE).toContain('terminal-history-wsl/Debian')
})
it('translates a POSIX cwd through the preferred WSL distro', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
await provider.spawn({
cols: 80,
rows: 24,
worktreeId: 'repo-1::C:\\Users\\jin\\repo',
cwd: '/home/jin/repo',
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: 'Debian'
})
const spawnCall = spawnMock.mock.calls.at(-1)!
expect(spawnCall[0]).toBe('wsl.exe')
expect(spawnCall[1]).toEqual([
'-d',
'Debian',
'--',
'sh',
'-c',
expect.stringContaining("cd '/home/jin/repo'")
])
expect(spawnCall[2].cwd).not.toBe('/home/jin/repo')
})
it('resolves and persists the default distro for Windows cwd WSL terminals', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const buildSpawnEnv = vi.fn(

View File

@ -239,9 +239,9 @@ function getWslContextFromWorktreeId(
*/
function getWslContextFromPreferredDistro(
distro: string | null | undefined
): { distro: string } | undefined {
): { distro: string; treatPosixCwdAsWsl: true } | undefined {
const trimmed = distro?.trim()
return trimmed ? { distro: trimmed } : undefined
return trimmed ? { distro: trimmed, treatPosixCwdAsWsl: true } : undefined
}
/**

View File

@ -29,7 +29,8 @@ import {
parseWslPath,
toLinuxPath,
toWindowsWslPath,
wslUncDirectoryExists
wslUncDirectoryExists,
wslUncDirectoryExistsAsync
} from './wsl'
function withPlatform<T>(value: NodeJS.Platform, fn: () => T): T {
@ -805,36 +806,39 @@ describe('wslUncDirectoryExists', () => {
})
it('returns true when the distro reports the directory exists', () => {
execFileSyncMock.mockReturnValue('')
execFileSyncMock.mockReturnValue('__ORCA_DIRECTORY_EXISTS__')
const result = withPlatform('win32', () =>
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
)
expect(result).toBe(true)
expect(execFileSyncMock).toHaveBeenCalledWith(
'wsl.exe',
['-d', 'Ubuntu', '--', 'test', '-d', '/home/jin/repo'],
[
'-d',
'Ubuntu',
'--',
'sh',
'-c',
expect.stringContaining('__ORCA_DIRECTORY_EXISTS__'),
'sh',
'/home/jin/repo'
],
expect.objectContaining({ timeout: 5000 })
)
})
it('returns false when test -d exits non-zero (directory missing)', () => {
execFileSyncMock.mockImplementation(() => {
// Why: child_process surfaces a non-zero exit as an Error with `status`.
const error = new Error('Command failed') as Error & { status: number }
error.status = 1
throw error
})
it('returns false when the guest reports the directory missing', () => {
execFileSyncMock.mockReturnValue('__ORCA_DIRECTORY_MISSING__')
const result = withPlatform('win32', () =>
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing')
)
expect(result).toBe(false)
})
it('returns null when wsl.exe is unavailable (inconclusive)', () => {
it('returns null when wsl.exe or the distro is unavailable', () => {
execFileSyncMock.mockImplementation(() => {
// No numeric `status` -> spawn failure (ENOENT), not a missing directory.
const error = new Error('spawn wsl.exe ENOENT') as Error & { code: string }
error.code = 'ENOENT'
const error = new Error('distro unavailable') as Error & { status: number }
error.status = 4294967295
throw error
})
const result = withPlatform('win32', () =>
@ -851,3 +855,67 @@ describe('wslUncDirectoryExists', () => {
expect(execFileSyncMock).not.toHaveBeenCalled()
})
})
describe('wslUncDirectoryExistsAsync', () => {
afterEach(() => {
execFileMock.mockReset()
})
it('returns true when the distro reports the directory exists', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) =>
callback(null, '__ORCA_DIRECTORY_EXISTS__')
)
await expect(
withPlatformAsync('win32', () =>
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
)
).resolves.toBe(true)
expect(execFileMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'sh',
'-c',
expect.stringContaining('__ORCA_DIRECTORY_EXISTS__'),
'sh',
'/home/jin/repo'
],
expect.objectContaining({ timeout: 5000 }),
expect.any(Function)
)
})
it('distinguishes a missing directory from an inconclusive probe', async () => {
execFileMock
.mockImplementationOnce((_command, _args, _options, callback) =>
callback(null, '__ORCA_DIRECTORY_MISSING__')
)
.mockImplementationOnce((_command, _args, _options, callback) =>
callback(Object.assign(new Error('distro unavailable'), { code: 4294967295 }), '')
)
await withPlatformAsync('win32', async () => {
await expect(
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing')
).resolves.toBe(false)
await expect(
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
).resolves.toBeNull()
})
})
it('returns null without spawning for paths outside WSL or off Windows', async () => {
await expect(
withPlatformAsync('win32', () => wslUncDirectoryExistsAsync('C:\\Users\\jin\\repo'))
).resolves.toBeNull()
await expect(
withPlatformAsync('linux', () =>
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin')
)
).resolves.toBeNull()
expect(execFileMock).not.toHaveBeenCalled()
})
})

View File

@ -21,6 +21,33 @@ export type WslPathInfo = {
linuxPath: string
}
const WSL_DIRECTORY_EXISTS_MARKER = '__ORCA_DIRECTORY_EXISTS__'
const WSL_DIRECTORY_MISSING_MARKER = '__ORCA_DIRECTORY_MISSING__'
function getWslDirectoryProbeArgs(info: WslPathInfo): string[] {
return [
'-d',
info.distro,
'--',
'sh',
'-c',
`if [ -d "$1" ]; then printf ${WSL_DIRECTORY_EXISTS_MARKER}; else printf ${WSL_DIRECTORY_MISSING_MARKER}; fi`,
'sh',
info.linuxPath
]
}
function parseWslDirectoryProbeOutput(stdout: unknown): boolean | null {
const output = String(stdout)
if (output.includes(WSL_DIRECTORY_EXISTS_MARKER)) {
return true
}
if (output.includes(WSL_DIRECTORY_MISSING_MARKER)) {
return false
}
return null
}
/**
* Detect if a Windows path is a WSL UNC path and extract the distro name
* and equivalent Linux path.
@ -49,8 +76,8 @@ export function isWslPath(path: string): boolean {
* Why: Win32 fs.statSync against the WSL 9P filesystem (\\wsl.localhost\...)
* is unreliable for repos that live on the WSL side it can report ENOENT for
* directories that exist, which made opening a WSL worktree fail with
* "Working directory ... does not exist". `wsl.exe -d <distro> test -d` asks
* the distro directly, which is the authoritative answer. Returns null (rather
* "Working directory ... does not exist". The guest marker probe asks the
* distro directly, which is the authoritative answer. Returns null (rather
* than false) when wsl.exe is unavailable or errors so callers can fall back to
* the fs check instead of falsely rejecting a valid directory.
*/
@ -63,22 +90,33 @@ export function wslUncDirectoryExists(uncPath: string): boolean | null {
return null
}
try {
execFileSync('wsl.exe', ['-d', info.distro, '--', 'test', '-d', info.linuxPath], {
const stdout = execFileSync('wsl.exe', getWslDirectoryProbeArgs(info), {
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 5000
timeout: 5000,
encoding: 'utf8'
})
return true
} catch (error) {
// A non-zero exit (directory missing) surfaces as an error with a numeric
// `status`; treat that as a definitive "does not exist". Any other failure
// (wsl.exe missing, distro not running, timeout) is inconclusive -> null.
if (typeof (error as { status?: unknown })?.status === 'number') {
return false
}
return parseWslDirectoryProbeOutput(stdout)
} catch {
return null
}
}
export function wslUncDirectoryExistsAsync(uncPath: string): Promise<boolean | null> {
if (process.platform !== 'win32') {
return Promise.resolve(null)
}
const info = parseWslUncPath(uncPath)
if (!info) {
return Promise.resolve(null)
}
return new Promise((resolve) => {
execFile('wsl.exe', getWslDirectoryProbeArgs(info), { timeout: 5000 }, (_error, stdout) => {
// Why: wsl.exe uses numeric exits for both guest results and host failures; only the guest marker is authoritative.
resolve(parseWslDirectoryProbeOutput(stdout))
})
})
}
/**
* Convert a Windows path to a Linux path for commands that will execute inside WSL.
* Returns the path unchanged if it is already POSIX-style.

View File

@ -260,6 +260,7 @@ export default function AiVaultSessionDropLayer({
agent: payload.agent,
worktreeId,
command: startup.command,
...(payload.sessionCwd ? { cwd: payload.sessionCwd } : {}),
...(startup.env ? { env: startup.env } : {}),
...(startup.envToDelete ? { envToDelete: startup.envToDelete } : {}),
...(startup.launchConfig ? { launchConfig: startup.launchConfig } : {}),

View File

@ -39,8 +39,8 @@ describe('AI Vault OMP cold resume', () => {
})
expect(startup).toMatchObject({
command:
"cd '/repo' && omp '--model' 'custom' --resume '/custom/omp-sessions/project/session.jsonl'",
command: "omp '--model' 'custom' --resume '/custom/omp-sessions/project/session.jsonl'",
cwd: '/repo',
env: { OMP_PROFILE: 'custom' },
launchConfig: {
agentCommand: "omp '--model' 'custom'",

View File

@ -64,7 +64,7 @@ describe('buildAiVaultDropRepinStartup', () => {
expect(startup).not.toBeNull()
expect(startup?.command).toContain(`CODEX_HOME='${SELECTED_HOME}'`)
expect(startup?.command).not.toContain(RECORDED_HOME)
expect(startup?.command).toContain("cd '/Users/ada/repo' && ")
expect(startup).toMatchObject({ cwd: '/Users/ada/repo' })
})
it('repins a payload whose session has no cwd instead of keeping the wrong-account command', () => {

View File

@ -69,6 +69,30 @@ function buildQueuedAiVaultResumeCommand(
}
describe('ai vault resume command runtime', () => {
it('repro: queues a host-runtime resume without configured-WSL shell syntax', () => {
const state = makeState({
worktreePath: 'C:\\Users\\alice\\repo',
localWindowsRuntimePreference: { kind: 'windows-host' },
terminalWindowsShell: 'wsl.exe'
})
expect(
buildAiVaultResumeStartupForWorktree({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'claude',
sessionId: 'session one',
cwd: 'C:\\Users\\alice\\repo',
codexHome: null
}
})
).toMatchObject({
command: "claude '--resume' 'session one'",
cwd: 'C:\\Users\\alice\\repo'
})
})
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).
@ -85,7 +109,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("Set-Location -LiteralPath 'C:\\Users\\alice\\repo'; claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('queues direct cmd syntax when the configured Windows shell is cmd.exe', () => {
@ -105,7 +129,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe('cd /d "C:\\Users\\alice\\repo" && claude "--resume" "session one"')
).toBe('claude "--resume" "session one"')
})
it('queues a POSIX command for the Git Bash Windows shell', () => {
@ -125,7 +149,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("cd 'C:\\Users\\alice\\repo' && claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('follows the live Windows shell for non-resumable agents in the fallback path', () => {
@ -144,9 +168,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe(
"Set-Location -LiteralPath 'C:\\Users\\alice\\repo'; cursor-agent --resume 'session one'"
)
).toBe("cursor-agent --resume 'session one'")
})
it('queues a PowerShell-valid local OMP resume by absolute transcript path', () => {
@ -166,9 +188,7 @@ describe('ai vault resume command runtime', () => {
}
})
expect(command).toBe(
"Set-Location -LiteralPath 'C:\\Users\\alice\\repo'; omp --resume 'C:\\Users\\alice\\.omp\\agent\\sessions\\repo\\sess.jsonl'"
)
expect(command).toBe("omp --resume 'C:\\Users\\alice\\.omp\\agent\\sessions\\repo\\sess.jsonl'")
expect(command).not.toContain('019f27cd-4268-7000-96e7-62f42a55c144')
})
@ -190,9 +210,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe(
'cd /d "C:\\Users\\alice\\repo" && omp --resume "C:\\Users\\alice\\.omp\\agent\\sessions\\repo\\sess.jsonl"'
)
).toBe('omp --resume "C:\\Users\\alice\\.omp\\agent\\sessions\\repo\\sess.jsonl"')
})
it('copies syntax that matches the configured cmd shell', () => {
@ -337,8 +355,8 @@ describe('ai vault resume command runtime', () => {
}
})
).toEqual({
command:
"cd '/home/alice/repo' && claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'",
command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'",
cwd: '/home/alice/repo',
env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' },
launchConfig: {
agentCommand: "claude '--dangerously-skip-permissions' '--effort' 'max'",
@ -367,7 +385,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("cd '/home/alice/repo' && claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('uses POSIX command wrapping for SSH-owned worktrees on Windows clients', () => {
@ -386,7 +404,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("cd '/home/alice/repo' && claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('uses POSIX command wrapping for folder workspaces with their own SSH target', () => {
@ -415,7 +433,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("cd '/home/alice/platform' && claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('uses POSIX command wrapping for WSL UNC folder workspaces on Windows clients', () => {
@ -443,7 +461,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe("cd '/home/alice/platform' && claude '--resume' 'session one'")
).toBe("claude '--resume' 'session one'")
})
it('keeps WSL UNC worktrees on POSIX command wrapping without an explicit override', () => {
@ -470,7 +488,7 @@ describe('ai vault resume command runtime', () => {
codexHome: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex'
}
})
).toBe("cd '/home/alice/repo' && CODEX_HOME='/home/alice/.codex' codex 'resume' 'session one'")
).toBe("CODEX_HOME='/home/alice/.codex' codex 'resume' 'session one'")
})
it('converts WSL UNC OMP transcript paths before building Linux resume commands', () => {
@ -491,9 +509,7 @@ describe('ai vault resume command runtime', () => {
codexHome: null
}
})
).toBe(
"cd '/home/alice/repo' && omp --resume '/home/alice/.omp/agent/sessions/repo/sess.jsonl'"
)
).toBe("omp --resume '/home/alice/.omp/agent/sessions/repo/sess.jsonl'")
})
it('deletes inherited Codex homes when resuming a real-home session', () => {
@ -511,7 +527,8 @@ describe('ai vault resume command runtime', () => {
}
})
).toMatchObject({
command: "Set-Location -LiteralPath '/home/alice/repo'; codex 'resume' 'session one'",
command: "codex 'resume' 'session one'",
cwd: '/home/alice/repo',
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME']
})
})
@ -534,7 +551,8 @@ describe('ai vault resume command runtime', () => {
}
})
).toMatchObject({
command: "cd '/home/alice/repo' && codex 'resume' 'session one'",
command: "codex 'resume' 'session one'",
cwd: '/home/alice/repo',
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'],
providerSession: { key: 'session_id', id: 'session one' }
})
@ -558,7 +576,7 @@ describe('ai vault resume command runtime', () => {
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("cd '/home/alice/repo' && codex 'resume' 'session one'")
).toBe("codex 'resume' 'session one'")
})
it('copies remote real-home Codex commands with explicit environment cleanup', () => {
@ -603,7 +621,7 @@ describe('ai vault resume command runtime', () => {
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("cd '/home/alice/repo' && my-codex 'resume' 'session one'")
).toBe("my-codex 'resume' 'session one'")
})
it('rebuilds overridden remote commands with the recorded remote host platform', () => {
@ -629,9 +647,7 @@ describe('ai vault resume command runtime', () => {
'cmd /d /s /c "cd /d ""C:/Users/alice/repo"" && set ""CODEX_HOME=C:/Users/alice/.codex"" && codex resume ""session one"""'
}
})
).toBe(
"Set-Location -LiteralPath 'C:/Users/alice/repo'; $env:CODEX_HOME='C:/Users/alice/.codex'; my-codex 'resume' 'session one'"
)
).toBe("$env:CODEX_HOME='C:/Users/alice/.codex'; my-codex 'resume' 'session one'")
})
it('ignores a stored resume command for local-host sessions', () => {
@ -651,6 +667,6 @@ describe('ai vault resume command runtime', () => {
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("cd '/home/alice/repo' && codex 'resume' 'session one'")
).toBe("codex 'resume' 'session one'")
})
})

View File

@ -15,13 +15,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 {
clearEnvCommand,
commandSeparator,
resolveStartupShell
} from '../../../shared/tui-agent-startup-shell'
import { clearEnvCommand, commandSeparator } from '../../../shared/tui-agent-startup-shell'
import type { AppState } from '@/store/types'
import type { AiVaultSessionDragPayload } from '@/lib/ai-vault-session-drag'
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
@ -29,7 +24,10 @@ import { CLIENT_PLATFORM } from '@/lib/new-workspace'
import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId } from '../../../shared/execution-host'
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
import {
getAiVaultResumeWorkspacePath,
resolveAiVaultResumeStartupShell
} from '@/lib/ai-vault-resume-shell'
type AiVaultResumeCommandSession = Pick<
AiVaultSession,
@ -41,6 +39,7 @@ type AiVaultResumeCommandSession = Pick<
export type AiVaultResumeStartup = {
command: string
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
@ -65,7 +64,7 @@ type AiVaultResumeWorktreeArgs = {
}
export function buildAiVaultResumeCopyCommandForWorktree(args: AiVaultResumeWorktreeArgs): string {
const command = buildAiVaultResumeForWorktree(args).command
const command = buildAiVaultResumeForWorktree(args, true).command
if (args.session.agent !== 'codex' || args.session.codexHome !== null) {
return command
}
@ -80,7 +79,7 @@ export function buildAiVaultResumeCopyCommandForWorktree(args: AiVaultResumeWork
export function buildAiVaultResumeStartupForWorktree(
args: AiVaultResumeWorktreeArgs
): AiVaultResumeStartup {
return buildAiVaultResumeForWorktree(args)
return buildAiVaultResumeForWorktree(args, false)
}
/**
@ -120,7 +119,10 @@ export function buildAiVaultDropRepinStartup(args: {
})
}
function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVaultResumeStartup {
function buildAiVaultResumeForWorktree(
args: AiVaultResumeWorktreeArgs,
embedCwd: boolean
): AiVaultResumeStartup {
const providerSession = getAiVaultAgentProviderSession(args.session)
if (
args.session.executionHostId &&
@ -151,9 +153,11 @@ function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVault
const liveShell: AgentStartupShell | undefined =
platform === 'win32'
? isLocalSession
? resolveWindowsShellStartupFamily(args.state.settings?.terminalWindowsShell)
? resolveAiVaultResumeShell(args)
: 'powershell'
: undefined
const cwd = embedCwd ? args.session.cwd : null
const startupCwd = !embedCwd && args.session.cwd ? { cwd: args.session.cwd } : {}
if (providerSession && isResumableTuiAgent(args.session.agent)) {
const startupPlan = buildAgentResumeStartupPlan({
agent: args.session.agent,
@ -181,7 +185,7 @@ function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVault
agent: args.session.agent,
sessionId: args.session.sessionId,
resumeFilePath,
cwd: args.session.cwd,
cwd,
platform,
commandOverride: startupPlan.launchConfig.agentCommand,
codexHome,
@ -189,13 +193,14 @@ function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVault
})
: buildAiVaultResumeShellCommand({
resumeCommand: startupPlan.launchCommand,
cwd: args.session.cwd,
cwd,
platform,
codexHome,
shell: liveShell
}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
...realHomeCodexResumeEnvDeletion(args.session),
...startupCwd,
launchConfig: startupPlan.launchConfig,
providerSession
}
@ -210,7 +215,7 @@ function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVault
// forward it too — otherwise a custom OMP_CODING_AGENT_DIR / WSL-store
// session would resume by id against the default store and miss.
resumeFilePath,
cwd: args.session.cwd,
cwd,
platform,
commandOverride: args.commandOverride,
codexHome,
@ -218,6 +223,7 @@ function buildAiVaultResumeForWorktree(args: AiVaultResumeWorktreeArgs): AiVault
// quote for the live Windows shell like the startup-plan branch above.
shell: liveShell
}),
...startupCwd,
...realHomeCodexResumeEnvDeletion(args.session)
}
}
@ -231,11 +237,12 @@ function resolveAiVaultResumeShell(args: AiVaultResumeWorktreeArgs): AgentStartu
: getAiVaultResumePlatform(args.state, args.worktreeId)
const isLocalSession =
!args.session.executionHostId || args.session.executionHostId === LOCAL_EXECUTION_HOST_ID
const shell =
platform === 'win32' && isLocalSession
? resolveWindowsShellStartupFamily(args.state.settings?.terminalWindowsShell)
: undefined
return resolveStartupShell(platform, shell)
return resolveAiVaultResumeStartupShell({
state: args.state,
worktreeId: args.worktreeId,
platform,
isLocalSession
})
}
export function getAiVaultAgentProviderSession(
@ -298,26 +305,3 @@ export function getAiVaultResumePlatform(
const workspacePath = getAiVaultResumeWorkspacePath(state, targetWorktreeId)
return workspacePath && parseWslUncPath(workspacePath) ? 'linux' : CLIENT_PLATFORM
}
function getAiVaultResumeWorkspacePath(
state: Pick<AppState, 'folderWorkspaces' | 'worktreesByRepo'>,
worktreeId: string | null | undefined
): string | null {
if (!worktreeId) {
return null
}
const workspaceScope = parseWorkspaceKey(worktreeId)
if (workspaceScope?.type === 'folder') {
return (
state.folderWorkspaces.find((workspace) => workspace.id === workspaceScope.folderWorkspaceId)
?.folderPath ?? null
)
}
const targetWorktreeId =
workspaceScope?.type === 'worktree' ? workspaceScope.worktreeId : worktreeId
return (
Object.values(state.worktreesByRepo ?? {})
.flat()
.find((candidate) => candidate.id === targetWorktreeId)?.path ?? null
)
}

View File

@ -0,0 +1,72 @@
import type { AppState } from '@/store/types'
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
import { resolveLocalWindowsTerminalShellOverrideForTab } from '../../../shared/local-windows-terminal-runtime'
import { resolveWindowsShellStartupFamily } from '../../../shared/windows-terminal-shell'
import {
resolveStartupShell,
type AgentStartupShell
} from '../../../shared/tui-agent-startup-shell'
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
import { parseWslUncPath } from '../../../shared/wsl-paths'
type AiVaultResumeShellState = Pick<
AppState,
| 'activeRepoId'
| 'activeWorktreeId'
| 'folderWorkspaces'
| 'projects'
| 'repos'
| 'settings'
| 'worktreesByRepo'
>
export function resolveAiVaultResumeStartupShell(args: {
state: AiVaultResumeShellState
worktreeId?: string | null
platform: NodeJS.Platform
isLocalSession: boolean
}): AgentStartupShell {
const projectRuntime =
args.platform === 'win32' && args.isLocalSession
? getLocalProjectExecutionRuntimeContext(args.state, args.worktreeId, CLIENT_PLATFORM)
: undefined
const workspacePath = getAiVaultResumeWorkspacePath(
args.state,
args.worktreeId ?? args.state.activeWorktreeId
)
const shellOverride =
args.platform === 'win32' && args.isLocalSession
? resolveLocalWindowsTerminalShellOverrideForTab({
explicitShellOverride: undefined,
defaultWindowsShell: args.state.settings?.terminalWindowsShell,
isWslWorktree: Boolean(workspacePath && parseWslUncPath(workspacePath)),
projectRuntime
})
: undefined
const shell = shellOverride ? resolveWindowsShellStartupFamily(shellOverride) : undefined
return resolveStartupShell(args.platform, shell)
}
export function getAiVaultResumeWorkspacePath(
state: Pick<AppState, 'folderWorkspaces' | 'worktreesByRepo'>,
worktreeId: string | null | undefined
): string | null {
if (!worktreeId) {
return null
}
const workspaceScope = parseWorkspaceKey(worktreeId)
if (workspaceScope?.type === 'folder') {
return (
state.folderWorkspaces.find((workspace) => workspace.id === workspaceScope.folderWorkspaceId)
?.folderPath ?? null
)
}
const targetWorktreeId =
workspaceScope?.type === 'worktree' ? workspaceScope.worktreeId : worktreeId
return (
Object.values(state.worktreesByRepo ?? {})
.flat()
.find((candidate) => candidate.id === targetWorktreeId)?.path ?? null
)
}

View File

@ -98,15 +98,20 @@ describe('launchAiVaultSessionInNewTab', () => {
agent: 'claude',
worktreeId: 'wt-1',
command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'",
cwd: 'C:\\Users\\alice\\repo',
env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' },
envToDelete: ['CODEX_HOME'],
launchConfig: {
agentCommand: "claude '--dangerously-skip-permissions' '--effort' 'max'",
agentArgs: '--dangerously-skip-permissions --effort max',
agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }
}
},
providerSession: { key: 'session_id', id: 'session-1' }
})
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
startupCwd: 'C:\\Users\\alice\\repo'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', {
command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'",
env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' },
@ -117,6 +122,7 @@ describe('launchAiVaultSessionInNewTab', () => {
agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }
},
launchAgent: 'claude',
resumeProviderSession: { key: 'session_id', id: 'session-1' },
telemetry: {
agent_kind: 'claude',
launch_source: 'sidebar',

View File

@ -22,6 +22,7 @@ export function launchAiVaultSessionInNewTab(args: {
agent: AiVaultAgent
worktreeId: string
command: string
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
@ -40,6 +41,7 @@ export function launchAiVaultSessionInNewTab(args: {
agentSessionKind: 'resume',
launchAgent: args.agent,
command: args.command,
...(args.cwd ? { cwd: args.cwd } : {}),
...(args.env ? { env: args.env } : {}),
...(args.envToDelete ? { envToDelete: args.envToDelete } : {}),
...(args.launchConfig ? { launchConfig: args.launchConfig } : {}),
@ -66,12 +68,15 @@ export function launchAiVaultSessionInNewTab(args: {
targetGroupId
}
const tab = store.createTab(args.worktreeId, targetGroupId)
const tab = args.cwd
? store.createTab(args.worktreeId, targetGroupId, undefined, { startupCwd: args.cwd })
: store.createTab(args.worktreeId, targetGroupId)
store.queueTabStartupCommand(tab.id, {
command: args.command,
...(args.env ? { env: args.env } : {}),
...(args.envToDelete ? { envToDelete: args.envToDelete } : {}),
...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}),
...(args.providerSession ? { resumeProviderSession: args.providerSession } : {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(args.agent),
launch_source: 'sidebar',