diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index aa3d3d6fb..39ac366ed 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -1,6 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { buildAiVaultResumeCommandForWorktree } from '@/lib/ai-vault-resume-command' +import { + buildAiVaultResumeCommandForWorktree, + buildAiVaultResumeStartupForWorktree +} from '@/lib/ai-vault-resume-command' import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session' import { useAppStore } from '@/store' import { @@ -46,7 +49,8 @@ export default function AiVaultPanel(): React.JSX.Element { const repos = useRepos() const allWorktrees = useAllWorktrees() const projectHostSetupProjection = useProjectHostSetupProjection() - const agentCmdOverrides = useAppStore((s) => s.settings?.agentCmdOverrides ?? {}) + const settings = useAppStore((s) => s.settings) + const agentCmdOverrides = settings?.agentCmdOverrides ?? {} const [query, setQuery] = useState('') const [scope, setScope] = useState(DEFAULT_AI_VAULT_SCOPE) const [sort, setSort] = useState('updated') @@ -207,7 +211,18 @@ export default function AiVaultPanel(): React.JSX.Element { session, commandOverride: agentCmdOverrides[session.agent] }), - [activeWorktree?.id, agentCmdOverrides] + [activeWorktree?.id, agentCmdOverrides, settings] + ) + + const buildResumeStartup = useCallback( + (session: AiVaultSession) => + buildAiVaultResumeStartupForWorktree({ + state: useAppStore.getState(), + worktreeId: activeWorktree?.id ?? null, + session, + commandOverride: agentCmdOverrides[session.agent] + }), + [activeWorktree?.id, agentCmdOverrides, settings] ) const copyResumeCommand = useCallback( @@ -255,7 +270,7 @@ export default function AiVaultPanel(): React.JSX.Element { launchAiVaultSessionInNewTab({ agent: session.agent, worktreeId: activeWorktree.id, - command: buildResumeCommand(session) + ...buildResumeStartup(session) }) toast.success( translate( @@ -265,7 +280,7 @@ export default function AiVaultPanel(): React.JSX.Element { ) ) }, - [activeWorktree, buildResumeCommand, isRemoteWorktree] + [activeWorktree, buildResumeStartup, isRemoteWorktree] ) const setAgentEnabled = useCallback((agent: AiVaultAgent, enabled: boolean) => { @@ -363,7 +378,7 @@ export default function AiVaultPanel(): React.JSX.Element { filteredSessionsCount={filteredSessions.length} error={error} resumeDisabled={!activeWorktree || isRemoteWorktree} - buildResumeCommand={buildResumeCommand} + buildResumeStartup={buildResumeStartup} onToggleGroup={toggleGroup} onResume={handleResume} onCopyResume={(session) => void copyResumeCommand(session)} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx index 1d147de5a..e63479ee2 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx @@ -16,6 +16,7 @@ import { writeAiVaultSessionDragData } from '@/lib/ai-vault-session-drag' import type { AiVaultSession } from '../../../../shared/ai-vault-types' +import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command' import { agentLabel } from './ai-vault-session-filters' import { translate } from '@/i18n/i18n' import { SessionInlineDetails, SessionTime } from './AiVaultSessionDetails' @@ -24,7 +25,7 @@ import { SessionRowTrailingActions } from './SessionRowTrailingActions' export function VaultSessionRow({ session, - resumeCommand, + resumeStartup, detailsExpanded, resumeDisabled, onToggleDetails, @@ -37,7 +38,7 @@ export function VaultSessionRow({ onOpenCwd }: { session: AiVaultSession - resumeCommand: string + resumeStartup: AiVaultResumeStartup detailsExpanded: boolean resumeDisabled: boolean onToggleDetails: () => void @@ -66,11 +67,13 @@ export function VaultSessionRow({ agent: session.agent, sessionId: session.sessionId, title: session.title, - command: resumeCommand + command: resumeStartup.command, + ...(resumeStartup.env ? { env: resumeStartup.env } : {}), + ...(resumeStartup.launchConfig ? { launchConfig: resumeStartup.launchConfig } : {}) }) window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_START_EVENT)) }, - [resumeDisabled, session.agent, session.sessionId, session.title, resumeCommand] + [resumeDisabled, session.agent, session.sessionId, session.title, resumeStartup] ) return ( diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx index 7b1746bc7..9a9cfdf7a 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx @@ -1,6 +1,7 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { useCallback, useMemo, useRef, useState } from 'react' import type { AiVaultSession } from '../../../../shared/ai-vault-types' +import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import { getActiveStickyHeaderIndexForScroll } from '../sidebar/worktree-list-virtual-rows' @@ -29,7 +30,7 @@ export function AiVaultSessionVirtualList({ filteredSessionsCount, error, resumeDisabled, - buildResumeCommand, + buildResumeStartup, onToggleGroup, onResume, onCopyResume, @@ -46,7 +47,7 @@ export function AiVaultSessionVirtualList({ filteredSessionsCount: number error: string | null resumeDisabled: boolean - buildResumeCommand: (session: AiVaultSession) => string + buildResumeStartup: (session: AiVaultSession) => AiVaultResumeStartup onToggleGroup: (key: string) => void onResume: (session: AiVaultSession) => void onCopyResume: (session: AiVaultSession) => void @@ -163,7 +164,7 @@ export function AiVaultSessionVirtualList({ collapsedGroups={collapsedGroups} expandedSessionIds={expandedSessionIds} resumeDisabled={resumeDisabled} - buildResumeCommand={buildResumeCommand} + buildResumeStartup={buildResumeStartup} onToggleGroup={onToggleGroup} onToggleSessionDetails={toggleSessionDetails} onResume={onResume} @@ -190,7 +191,7 @@ function AiVaultVirtualRow({ collapsedGroups, expandedSessionIds, resumeDisabled, - buildResumeCommand, + buildResumeStartup, onToggleGroup, onToggleSessionDetails, onResume, @@ -209,7 +210,7 @@ function AiVaultVirtualRow({ collapsedGroups: ReadonlySet expandedSessionIds: ReadonlySet resumeDisabled: boolean - buildResumeCommand: (session: AiVaultSession) => string + buildResumeStartup: (session: AiVaultSession) => AiVaultResumeStartup onToggleGroup: (key: string) => void onToggleSessionDetails: (sessionId: string) => void onResume: (session: AiVaultSession) => void @@ -245,7 +246,7 @@ function AiVaultVirtualRow({ ) : ( onToggleSessionDetails(row.session.id)} diff --git a/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx b/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx index 91587d9d0..f9d791d3c 100644 --- a/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx +++ b/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx @@ -189,6 +189,8 @@ export default function AiVaultSessionDropLayer({ agent: payload.agent, worktreeId, command: payload.command, + ...(payload.env ? { env: payload.env } : {}), + ...(payload.launchConfig ? { launchConfig: payload.launchConfig } : {}), targetGroupId: dropTarget.groupId, splitDirection: dropTarget.zone === 'center' ? undefined : dropTarget.zone }) diff --git a/src/renderer/src/lib/ai-vault-resume-command.test.ts b/src/renderer/src/lib/ai-vault-resume-command.test.ts index 1d44f82a7..333fd6ded 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store/types' import { buildAiVaultResumeCommandForWorktree, + buildAiVaultResumeStartupForWorktree, getAiVaultResumePlatform } from './ai-vault-resume-command' @@ -31,7 +32,11 @@ function makeState(args: { : {}) } ], - settings: { localWindowsRuntimeDefault: { kind: 'windows-host' } }, + settings: { + localWindowsRuntimeDefault: { kind: 'windows-host' }, + agentDefaultArgs: { claude: '', codex: '' }, + agentDefaultEnv: { claude: {}, codex: {} } + }, worktreesByRepo: { 'repo-1': [ { @@ -62,7 +67,41 @@ describe('ai vault resume command runtime', () => { codexHome: null } }) - ).toBe('cmd /d /s /c "cd /d ""C:\\Users\\alice\\repo"" && claude --resume ""session one"""') + ).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', + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + state.settings = { + ...state.settings, + agentDefaultArgs: { claude: '--dangerously-skip-permissions --effort max' }, + agentDefaultEnv: { claude: { ANTHROPIC_BASE_URL: 'https://claude.example.test' } } + } as never + + expect( + buildAiVaultResumeStartupForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + session: { + agent: 'claude', + sessionId: 'session-1', + cwd: '/home/alice/repo', + codexHome: null + } + }) + ).toEqual({ + command: + "cd '/home/alice/repo' && claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'", + env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, + launchConfig: { + agentCommand: "claude '--dangerously-skip-permissions' '--effort' 'max'", + agentArgs: '--dangerously-skip-permissions --effort max', + agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' } + } + }) }) it('uses POSIX command wrapping for Windows-path projects forced to WSL', () => { @@ -83,7 +122,7 @@ describe('ai vault resume command runtime', () => { codexHome: null } }) - ).toBe("cd '/home/alice/repo' && claude --resume 'session one'") + ).toBe("cd '/home/alice/repo' && claude '--resume' 'session one'") }) it('keeps WSL UNC worktrees on POSIX command wrapping without an explicit override', () => { @@ -110,6 +149,6 @@ 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("cd '/home/alice/repo' && CODEX_HOME='/home/alice/.codex' codex 'resume' 'session one'") }) }) diff --git a/src/renderer/src/lib/ai-vault-resume-command.ts b/src/renderer/src/lib/ai-vault-resume-command.ts index eb6280235..400c54bdc 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.ts @@ -1,11 +1,30 @@ -import { buildAiVaultResumeCommand, type AiVaultSession } from '../../../shared/ai-vault-types' +import { + buildAiVaultResumeCommand, + buildAiVaultResumeShellCommand, + type AiVaultSession +} from '../../../shared/ai-vault-types' +import { + isResumableTuiAgent, + type SleepingAgentLaunchConfig +} from '../../../shared/agent-session-resume' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { parseWslUncPath } from '../../../shared/wsl-paths' import type { AppState } from '@/store/types' import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' import { CLIENT_PLATFORM } from '@/lib/new-workspace' +import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' type AiVaultResumeCommandSession = Pick +export type AiVaultResumeStartup = { + command: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig +} + export function buildAiVaultResumeCommandForWorktree(args: { state: Pick< AppState, @@ -15,16 +34,62 @@ export function buildAiVaultResumeCommandForWorktree(args: { session: AiVaultResumeCommandSession commandOverride?: string | null }): string { + return buildAiVaultResumeStartupForWorktree(args).command +} + +export function buildAiVaultResumeStartupForWorktree(args: { + state: Pick< + AppState, + 'activeRepoId' | 'activeWorktreeId' | 'projects' | 'repos' | 'settings' | 'worktreesByRepo' + > + worktreeId?: string | null + session: AiVaultResumeCommandSession + commandOverride?: string | null +}): AiVaultResumeStartup { const platform = getAiVaultResumePlatform(args.state, args.worktreeId) const codexHome = getAiVaultResumeCodexHome(args.session.codexHome, platform) - return buildAiVaultResumeCommand({ - agent: args.session.agent, - sessionId: args.session.sessionId, - cwd: args.session.cwd, - platform, - commandOverride: args.commandOverride, - codexHome - }) + if (isResumableTuiAgent(args.session.agent)) { + const startupPlan = buildAgentResumeStartupPlan({ + agent: args.session.agent, + providerSession: { key: 'session_id', id: args.session.sessionId }, + cmdOverrides: { + ...args.state.settings?.agentCmdOverrides, + ...(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, + agentArgs: resolveTuiAgentLaunchArgs( + args.session.agent, + args.state.settings?.agentDefaultArgs + ), + agentEnv: resolveTuiAgentLaunchEnv(args.session.agent, args.state.settings?.agentDefaultEnv) + }) + if (startupPlan) { + return { + command: buildAiVaultResumeShellCommand({ + resumeCommand: startupPlan.launchCommand, + cwd: args.session.cwd, + platform, + codexHome + }), + ...(startupPlan.env ? { env: startupPlan.env } : {}), + launchConfig: startupPlan.launchConfig + } + } + } + + return { + command: buildAiVaultResumeCommand({ + agent: args.session.agent, + sessionId: args.session.sessionId, + cwd: args.session.cwd, + platform, + commandOverride: args.commandOverride, + codexHome + }) + } } function getAiVaultResumeCodexHome( diff --git a/src/renderer/src/lib/ai-vault-session-drag.test.ts b/src/renderer/src/lib/ai-vault-session-drag.test.ts index f1a4d2ed8..6e1a93f76 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.test.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.test.ts @@ -47,7 +47,13 @@ describe('Session History session drag data', () => { agent: 'claude', sessionId: 'session-1', title: 'Fix terminal split', - command: "cd '/repo' && claude --resume session-1" + command: "cd '/repo' && claude --resume session-1", + env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, + launchConfig: { + agentCommand: 'claude --dangerously-skip-permissions', + agentArgs: '--dangerously-skip-permissions', + agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' } + } } writeAiVaultSessionDragData(transfer, payload) @@ -67,6 +73,45 @@ describe('Session History session drag data', () => { expect(readAiVaultSessionDragData(transfer)).toBeNull() }) + it('rejects array-shaped env records', () => { + const transfer = createTransfer() + transfer.setData( + AI_VAULT_SESSION_DRAG_TYPE, + JSON.stringify({ + kind: 'ai-vault-session', + version: 1, + agent: 'claude', + sessionId: 'session-1', + title: 'Malformed env', + command: 'claude --resume session-1', + env: ['ANTHROPIC_BASE_URL=https://claude.example.test'] + }) + ) + + expect(readAiVaultSessionDragData(transfer)).toBeNull() + }) + + it('rejects array-shaped launch config env records', () => { + const transfer = createTransfer() + transfer.setData( + AI_VAULT_SESSION_DRAG_TYPE, + JSON.stringify({ + kind: 'ai-vault-session', + version: 1, + agent: 'claude', + sessionId: 'session-1', + title: 'Malformed launch config env', + command: 'claude --resume session-1', + launchConfig: { + agentArgs: '', + agentEnv: ['ANTHROPIC_BASE_URL=https://claude.example.test'] + } + }) + ) + + expect(readAiVaultSessionDragData(transfer)).toBeNull() + }) + it('rejects oversized serialized payloads before parsing', () => { const transfer = createTransfer() const secret = 'ai-vault-drag-secret' diff --git a/src/renderer/src/lib/ai-vault-session-drag.ts b/src/renderer/src/lib/ai-vault-session-drag.ts index ef6bb39fb..1dfede058 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.ts @@ -1,4 +1,5 @@ import { AI_VAULT_AGENTS, type AiVaultAgent } from '../../../shared/ai-vault-types' +import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume' import { measureClipboardTextByteLength } from '../../../shared/clipboard-text' export const AI_VAULT_SESSION_DRAG_TYPE = 'application/x-orca-ai-vault-session' @@ -11,6 +12,9 @@ export type AiVaultSessionDragPayload = { sessionId: string title: string command: string + // Why: drag/drop resume must preserve planned env/default args, not just the shell command. + env?: Record + launchConfig?: SleepingAgentLaunchConfig } let activeAiVaultSessionDragPayload: AiVaultSessionDragPayload | null = null @@ -28,6 +32,25 @@ function isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value.trim().length > 0 } +function isStringRecord(value: unknown): value is Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false + } + return Object.values(value).every((entry) => typeof entry === 'string') +} + +function isLaunchConfig(value: unknown): value is SleepingAgentLaunchConfig { + if (!value || typeof value !== 'object') { + return false + } + const config = value as Partial + return ( + (config.agentCommand === undefined || typeof config.agentCommand === 'string') && + typeof config.agentArgs === 'string' && + isStringRecord(config.agentEnv) + ) +} + function isSerializedPayload(value: unknown): value is SerializedAiVaultSessionDragPayload { if (!value || typeof value !== 'object') { return false @@ -39,7 +62,9 @@ function isSerializedPayload(value: unknown): value is SerializedAiVaultSessionD isAiVaultAgent(payload.agent) && isNonEmptyString(payload.sessionId) && isNonEmptyString(payload.title) && - isNonEmptyString(payload.command) + isNonEmptyString(payload.command) && + (payload.env === undefined || isStringRecord(payload.env)) && + (payload.launchConfig === undefined || isLaunchConfig(payload.launchConfig)) ) } @@ -85,8 +110,15 @@ export function readAiVaultSessionDragData( if (!isSerializedPayload(parsed)) { return null } - const { agent, sessionId, title, command } = parsed - return { agent, sessionId, title, command } + const { agent, sessionId, title, command, env, launchConfig } = parsed + return { + agent, + sessionId, + title, + command, + ...(env ? { env } : {}), + ...(launchConfig ? { launchConfig } : {}) + } } catch { return null } diff --git a/src/renderer/src/lib/launch-ai-vault-session.test.ts b/src/renderer/src/lib/launch-ai-vault-session.test.ts index 3d33c6735..e6a1c083a 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.test.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.test.ts @@ -76,6 +76,36 @@ describe('launchAiVaultSessionInNewTab', () => { expect(result).toEqual({ tabId: 'tab-1', groupId: 'group-1' }) }) + it('queues configured resume startup details for agent history resumes', () => { + launchAiVaultSessionInNewTab({ + agent: 'claude', + worktreeId: 'wt-1', + command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'", + env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, + launchConfig: { + agentCommand: "claude '--dangerously-skip-permissions' '--effort' 'max'", + agentArgs: '--dangerously-skip-permissions --effort max', + agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' } + } + }) + + expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'", + env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, + launchConfig: { + agentCommand: "claude '--dangerously-skip-permissions' '--effort' 'max'", + agentArgs: '--dangerously-skip-permissions --effort max', + agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' } + }, + launchAgent: 'claude', + telemetry: { + agent_kind: 'claude', + launch_source: 'sidebar', + request_kind: 'resume' + } + }) + }) + it('creates a split group before launching when a split direction is provided', () => { launchAiVaultSessionInNewTab({ agent: 'codex', diff --git a/src/renderer/src/lib/launch-ai-vault-session.ts b/src/renderer/src/lib/launch-ai-vault-session.ts index 7db255779..a22f00271 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.ts @@ -2,12 +2,15 @@ import { useAppStore } from '@/store' import { reconcileTabOrder } from '@/components/tab-bar/reconcile-order' import { tuiAgentToAgentKind } from '@/lib/telemetry' import type { AiVaultAgent } from '../../../shared/ai-vault-types' +import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume' import type { TabSplitDirection } from '@/store/slices/tabs' export function launchAiVaultSessionInNewTab(args: { agent: AiVaultAgent worktreeId: string command: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig targetGroupId?: string splitDirection?: TabSplitDirection }): { tabId: string; groupId?: string } { @@ -22,6 +25,8 @@ export function launchAiVaultSessionInNewTab(args: { const tab = store.createTab(args.worktreeId, targetGroupId) store.queueTabStartupCommand(tab.id, { command: args.command, + ...(args.env ? { env: args.env } : {}), + ...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}), telemetry: { agent_kind: tuiAgentToAgentKind(args.agent), launch_source: 'sidebar', diff --git a/src/shared/ai-vault-types.ts b/src/shared/ai-vault-types.ts index b977489b4..8df6d611d 100644 --- a/src/shared/ai-vault-types.ts +++ b/src/shared/ai-vault-types.ts @@ -93,11 +93,21 @@ export function buildAiVaultResumeCommand(args: { const { agent, sessionId, cwd, platform, commandOverride, codexHome } = args const baseCommand = commandOverride?.trim() || defaultAiVaultResumeCommandBase(agent) const sessionArg = quoteShellArg(sessionId, platform) - const resumeCommand = buildAgentResumeInvocation(agent, baseCommand, sessionArg, { - codexHome: codexHome?.trim() || null, - platform - }) + const resumeCommand = buildAgentResumeInvocation(agent, baseCommand, sessionArg) + return buildAiVaultResumeShellCommand({ resumeCommand, cwd, platform, codexHome }) +} + +export function buildAiVaultResumeShellCommand(args: { + resumeCommand: string + cwd: string | null + platform: NodeJS.Platform + codexHome?: string | null +}): string { + const { cwd, platform, codexHome } = args + const resumeCommand = `${codexHomeEnvPrefix(codexHome?.trim() || null, platform)}${ + args.resumeCommand + }` if (!cwd) { return resumeCommand } @@ -130,12 +140,11 @@ function defaultAiVaultResumeCommandBase(agent: AiVaultAgent): string { function buildAgentResumeInvocation( agent: AiVaultAgent, baseCommand: string, - sessionArg: string, - options: { codexHome: string | null; platform: NodeJS.Platform } + sessionArg: string ): string { switch (agent) { case 'codex': - return `${codexHomeEnvPrefix(options.codexHome, options.platform)}${baseCommand} resume ${sessionArg}` + return `${baseCommand} resume ${sessionArg}` case 'rovo': return `${baseCommand} rovodev run --restore ${sessionArg}` case 'opencode':