Apply agent defaults to AI Vault resume (#6299)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
7436a4cfc1
commit
19b0174dca
|
|
@ -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<AiVaultScope>(DEFAULT_AI_VAULT_SCOPE)
|
||||
const [sort, setSort] = useState<AiVaultSort>('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)}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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<string>
|
||||
expandedSessionIds: ReadonlySet<string>
|
||||
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({
|
|||
) : (
|
||||
<VaultSessionRow
|
||||
session={row.session}
|
||||
resumeCommand={buildResumeCommand(row.session)}
|
||||
resumeStartup={buildResumeStartup(row.session)}
|
||||
detailsExpanded={expandedSessionIds.has(row.session.id)}
|
||||
resumeDisabled={resumeDisabled}
|
||||
onToggleDetails={() => onToggleSessionDetails(row.session.id)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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'")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<AiVaultSession, 'agent' | 'sessionId' | 'cwd' | 'codexHome'>
|
||||
|
||||
export type AiVaultResumeStartup = {
|
||||
command: string
|
||||
env?: Record<string, string>
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
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<string, string> {
|
||||
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<SleepingAgentLaunchConfig>
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
Loading…
Reference in New Issue