mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
* fix(ai-vault): index Cline sessions * fix(ai-vault): constrain Cline session discovery
271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
// Resume-command construction for Agent Session History rows: turns a scanned
|
|
// session into the shell line that re-enters it, quoted for the target platform
|
|
// and (when known) the live tab's shell.
|
|
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
|
import {
|
|
clearEnvCommand,
|
|
commandSeparator,
|
|
isPosixStartupShell,
|
|
quoteStartupArg,
|
|
type AgentStartupShell,
|
|
withoutEnvCommand
|
|
} from './tui-agent-startup-shell'
|
|
import type { AiVaultAgent, AiVaultSession } from './ai-vault-types'
|
|
|
|
export function buildAiVaultResumeCommand(args: {
|
|
agent: AiVaultAgent
|
|
sessionId: string
|
|
cwd: string | null
|
|
platform: NodeJS.Platform
|
|
commandOverride?: string | null
|
|
codexHome?: string | null
|
|
resumeFilePath?: string | null
|
|
shell?: AgentStartupShell
|
|
clearEnvNames?: readonly string[]
|
|
}): string {
|
|
const { agent, sessionId, cwd, platform, commandOverride, codexHome, resumeFilePath, shell } =
|
|
args
|
|
const baseCommand = commandOverride?.trim() || defaultAiVaultResumeCommandBase(agent)
|
|
// Why: OMP's and Prime Agent's `--resume` accept an absolute transcript path,
|
|
// which resolves regardless of which session-dir root (custom
|
|
// OMP_CODING_AGENT_DIR / PRIME_AGENT_CODING_AGENT_DIR / WSL home) the file was
|
|
// discovered under, where an id-prefix lookup scoped to the default store
|
|
// would miss it. Falls back to the id if no path is known.
|
|
const resumeTarget =
|
|
(agent === 'omp' || agent === 'prime-agent') && resumeFilePath?.trim()
|
|
? resumeFilePath.trim()
|
|
: sessionId
|
|
const sessionArg =
|
|
shell === 'cmd'
|
|
? quoteWindowsCmdArg(resumeTarget)
|
|
: shell
|
|
? quoteStartupArg(resumeTarget, shell)
|
|
: quoteShellArg(resumeTarget, platform)
|
|
const resumeCommand = buildAgentResumeInvocation(agent, baseCommand, sessionArg)
|
|
|
|
return buildAiVaultResumeShellCommand({
|
|
resumeCommand,
|
|
cwd,
|
|
platform,
|
|
codexHome,
|
|
shell,
|
|
clearEnvNames: args.clearEnvNames
|
|
})
|
|
}
|
|
|
|
export function buildAiVaultResumeShellCommand(args: {
|
|
resumeCommand: string
|
|
cwd: string | null
|
|
platform: NodeJS.Platform
|
|
codexHome?: string | null
|
|
/** Env names the agent must not inherit. Applied as a prefix on the agent
|
|
* itself, never on the whole `cd … && agent` chain — `cd` is a shell builtin
|
|
* that `env` cannot run, and a child `cd` would not move the agent anyway. */
|
|
clearEnvNames?: readonly string[]
|
|
// Why: the QUEUED resume command is typed into the live tab shell, so its
|
|
// cd/env prefix must match that shell. Shell-less persisted commands keep the
|
|
// legacy self-contained `cmd /d /s /c` wrapper.
|
|
shell?: AgentStartupShell
|
|
}): string {
|
|
const { cwd, platform, codexHome, shell, clearEnvNames } = args
|
|
|
|
// Why: shell-aware commands are parsed by a known running shell, while
|
|
// shell-less persisted commands keep the legacy self-contained cmd wrapper.
|
|
// PowerShell routes here whatever the platform: it is the shell, not the
|
|
// host, that decides which grammar the line has to be written in.
|
|
if (shell === 'powershell' || (platform === 'win32' && shell && shell !== 'cmd')) {
|
|
return buildResumeShellCommandForShell({
|
|
resumeCommand: args.resumeCommand,
|
|
cwd,
|
|
codexHome: codexHome?.trim() || null,
|
|
shell,
|
|
clearEnvNames
|
|
})
|
|
}
|
|
|
|
const resolvedCodexHome = codexHome?.trim() || null
|
|
// Why filter: the prefix and the removal name the same variable, and `env -u`
|
|
// strips what the assignment just set, so an unfiltered list would silently
|
|
// resume against the real home. Keeping the assignment authoritative matches
|
|
// the old `clear…; CODEX_HOME=x agent` ordering.
|
|
const clearNames = resolvedCodexHome
|
|
? clearEnvNames?.filter((name) => name !== 'CODEX_HOME')
|
|
: clearEnvNames
|
|
// Why the two placements differ: `set -u` aborts on the unbound `$fish_pid`
|
|
// the POSIX clear statement has to test, so there it must not precede the
|
|
// agent — `env -u` carries the removal on the agent itself. cmd has no such
|
|
// hazard, and keeping its clear ahead of the `cd` preserves `cd … && agent`,
|
|
// so a failed `cd` still cannot run the agent in the wrong directory.
|
|
// Keyed on the shell, not the platform: the shell is what picks the grammar.
|
|
const dialect = shell ?? (platform === 'win32' ? 'cmd' : 'posix')
|
|
const clearsOnAgent = clearNames?.length && isPosixStartupShell(dialect)
|
|
const resumeCommand = `${codexHomeEnvPrefix(resolvedCodexHome, platform, shell)}${
|
|
clearsOnAgent ? withoutEnvCommand(clearNames, args.resumeCommand, dialect) : args.resumeCommand
|
|
}`
|
|
const clearPrefix =
|
|
clearNames?.length && !clearsOnAgent
|
|
? `${clearEnvCommand(clearNames, dialect)}${commandSeparator(dialect)}`
|
|
: ''
|
|
if (platform === 'win32' && shell === 'cmd') {
|
|
// Why: an interactive cmd splits the doubled quotes required by a nested
|
|
// `cmd /s /c` wrapper, so queued commands must use direct cmd syntax.
|
|
return `${clearPrefix}${cwd ? `cd /d ${quoteWindowsCmdArg(cwd)} && ${resumeCommand}` : resumeCommand}`
|
|
}
|
|
if (!cwd) {
|
|
return `${clearPrefix}${resumeCommand}`
|
|
}
|
|
|
|
if (platform === 'win32') {
|
|
const inner = `${clearPrefix}cd /d ${quoteWindowsCmdArg(cwd)} && ${resumeCommand}`
|
|
return `cmd /d /s /c ${quoteWindowsCmdArg(inner)}`
|
|
}
|
|
|
|
return `cd ${quoteResumeArg(cwd, platform, shell)} && ${resumeCommand}`
|
|
}
|
|
|
|
function buildResumeShellCommandForShell(args: {
|
|
resumeCommand: string
|
|
cwd: string | null
|
|
codexHome: string | null
|
|
shell: Exclude<AgentStartupShell, 'cmd'>
|
|
clearEnvNames?: readonly string[]
|
|
}): string {
|
|
const { cwd, codexHome, shell, clearEnvNames } = args
|
|
if (isPosixStartupShell(shell)) {
|
|
// 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)} ` : ''
|
|
// Why filter: see the twin in buildAiVaultResumeShellCommand — `env -u`
|
|
// would strip the home the prefix just set.
|
|
const clearNames = codexHome
|
|
? clearEnvNames?.filter((name) => name !== 'CODEX_HOME')
|
|
: clearEnvNames
|
|
const command = `${envPrefix}${
|
|
clearNames?.length
|
|
? withoutEnvCommand(clearNames, args.resumeCommand, shell)
|
|
: args.resumeCommand
|
|
}`
|
|
return cwd ? `cd ${quoteStartupArg(cwd, shell)} && ${command}` : command
|
|
}
|
|
|
|
const separator = commandSeparator(shell)
|
|
const segments: string[] = []
|
|
// Why ahead of Set-Location: PowerShell has no `set -u` expansion hazard, so
|
|
// the removal keeps its original leading position.
|
|
if (clearEnvNames?.length) {
|
|
segments.push(clearEnvCommand(clearEnvNames, shell))
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Why: a bare real-home resume carries no CODEX_HOME prefix, so every surface
|
|
// that spawns the pane must drop account-routed or daemon-inherited Codex
|
|
// homes from its env, not only patch a sparse env on top.
|
|
export function realHomeCodexResumeEnvDeletion(
|
|
session: Pick<AiVaultSession, 'agent' | 'codexHome'>
|
|
): { envToDelete: string[] } | Record<string, never> {
|
|
if (session.agent !== 'codex' || session.codexHome !== null) {
|
|
return {}
|
|
}
|
|
return { envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'] }
|
|
}
|
|
|
|
function defaultAiVaultResumeCommandBase(agent: AiVaultAgent): string {
|
|
if (agent === 'cursor') {
|
|
return 'cursor-agent'
|
|
}
|
|
if (agent === 'hermes') {
|
|
return 'hermes'
|
|
}
|
|
if (agent === 'rovo') {
|
|
return 'acli'
|
|
}
|
|
return TUI_AGENT_CONFIG[agent].detectCmd
|
|
}
|
|
|
|
function buildAgentResumeInvocation(
|
|
agent: AiVaultAgent,
|
|
baseCommand: string,
|
|
sessionArg: string
|
|
): string {
|
|
switch (agent) {
|
|
case 'codex':
|
|
return `${baseCommand} resume ${sessionArg}`
|
|
case 'rovo':
|
|
return `${baseCommand} rovodev run --restore ${sessionArg}`
|
|
case 'opencode':
|
|
case 'pi':
|
|
// Why: Kimi Code resumes with `kimi --session <id>` (alias `-S`). Sessions
|
|
// are work-dir-scoped, so the cwd prefix from buildAiVaultResumeCommand is
|
|
// required — resuming from another directory is rejected by the CLI.
|
|
// falls through
|
|
case 'kimi':
|
|
return `${baseCommand} --session ${sessionArg}`
|
|
case 'copilot':
|
|
return `${baseCommand} --resume=${sessionArg}`
|
|
case 'cline':
|
|
return `${baseCommand} --id ${sessionArg}`
|
|
case 'claude':
|
|
case 'cursor':
|
|
case 'gemini':
|
|
case 'grok':
|
|
case 'hermes':
|
|
case 'devin':
|
|
case 'openclaw':
|
|
case 'droid':
|
|
// Why: OMP and Prime Agent resume by absolute transcript path (see
|
|
// buildAiVaultResumeCommand), but the `--resume <arg>` invocation form is
|
|
// identical to the others here.
|
|
// falls through
|
|
case 'omp':
|
|
case 'prime-agent':
|
|
return `${baseCommand} --resume ${sessionArg}`
|
|
case 'antigravity':
|
|
return `${baseCommand} --conversation ${sessionArg}`
|
|
}
|
|
}
|
|
|
|
function codexHomeEnvPrefix(
|
|
codexHome: string | null,
|
|
platform: NodeJS.Platform,
|
|
shell?: AgentStartupShell
|
|
): string {
|
|
if (!codexHome) {
|
|
return ''
|
|
}
|
|
if (platform === 'win32') {
|
|
return `set ${quoteWindowsCmdArg(`CODEX_HOME=${codexHome}`)} && `
|
|
}
|
|
// fish accepts the `NAME=value cmd` prefix (3.1+), but not sh's quoting.
|
|
return `CODEX_HOME=${quoteResumeArg(codexHome, platform, shell)} `
|
|
}
|
|
|
|
/** Quotes for the live shell when one is known, else for the platform's default. */
|
|
function quoteResumeArg(
|
|
value: string,
|
|
platform: NodeJS.Platform,
|
|
shell?: AgentStartupShell
|
|
): string {
|
|
return shell ? quoteStartupArg(value, shell) : quoteShellArg(value, platform)
|
|
}
|
|
|
|
/** Why not the sh `'\''` idiom here: this is the same resume command the shell
|
|
* branch above builds, and fish reads that idiom differently — it would halve
|
|
* backslashes in a path and reject a trailing one. Deferring to
|
|
* `quoteStartupArg` keeps one spelling regardless of whether a caller happened
|
|
* to pass a shell. */
|
|
function quoteShellArg(value: string, platform: NodeJS.Platform): string {
|
|
return platform === 'win32' ? quoteWindowsCmdArg(value) : quoteStartupArg(value, 'posix')
|
|
}
|
|
|
|
function quoteWindowsCmdArg(value: string): string {
|
|
return `"${value.replace(/"/g, '""')}"`
|
|
}
|