Files
orca/src/main/runtime/qoder-terminal-command-resolution.ts
T
Neil ebeea319b2 Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback.

Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path.

Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior.

Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented.
2026-10-04 19:50:45 -07:00

97 lines
3.4 KiB
TypeScript

import type { PreflightRuntimeContext } from '../preflight/agent-detection'
import { detectAgentCommandsOnHost } from '../preflight/agent-detection'
import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
import {
resolveStartupShell,
tokenizeStartupCommand,
type AgentStartupShell
} from '../../shared/tui-agent-startup-shell'
import type { TerminalCreateOptions } from './runtime-terminal-contracts'
import type { RuntimeStore } from './runtime-store-contract'
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
import { resolveLocalProjectRuntimeForWorktreeId } from '../local-project-runtime-resolution'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell'
export async function resolveQoderTerminalCommandForWorkspace(
launch: TerminalCreateOptions,
workspace: TerminalWorkspaceLaunchScope,
store: RuntimeStore | null | undefined,
platform: NodeJS.Platform
): Promise<TerminalCreateOptions> {
if (launch.launchAgent !== 'qoder') {
return launch
}
return resolveQoderTerminalCommand(launch, {
connectionId: workspace.connectionId,
context:
!workspace.connectionId && store
? {
projectRuntime: resolveLocalProjectRuntimeForWorktreeId(store, workspace.id),
wslDistro: parseWslUncPath(workspace.path)?.distro
}
: undefined,
shell: resolveStartupShell(
platform,
resolveLocalWindowsAgentStartupShell({
platform,
isRemote: Boolean(workspace.connectionId),
terminalWindowsShell: launch.shellOverride ?? store?.getSettings().terminalWindowsShell
})
)
})
}
export async function resolveQoderTerminalCommand<
T extends Pick<TerminalCreateOptions, 'launchAgent' | 'command' | 'launchConfig'>
>(
options: T,
host: {
connectionId?: string | null
context?: PreflightRuntimeContext
shell: AgentStartupShell
},
detect = detectAgentCommandsOnHost
): Promise<T> {
if (options.launchAgent !== 'qoder' || !options.command) {
return options
}
const parsed = tokenizeStartupCommand(options.command, host.shell)
// Explicit paths and configured recipes remain owned by the caller.
if (!parsed.ok) {
return options
}
const executableIndex = parsed.tokens.findIndex(
(token, index) =>
token === TUI_AGENT_CONFIG.qoder.detectCmd &&
(index === 0 ||
parsed.tokens[index - 1] === '&&' ||
(parsed.tokens[0] === 'Set-Location' && parsed.tokens[index - 1].endsWith(';')))
)
if (executableIndex === -1) {
return options
}
const candidates = getTuiAgentDetectCommands(TUI_AGENT_CONFIG.qoder)
const found = await detect(candidates, host)
const selected = candidates.find((candidate) => found.has(candidate))
if (!selected || selected === parsed.tokens[executableIndex]) {
return options
}
const first = parsed.spans[executableIndex]
if (!first) {
return options
}
return {
...options,
command: options.command.slice(0, first.start) + selected + options.command.slice(first.end),
...(options.launchConfig
? {
launchConfig: {
...options.launchConfig,
agentCommand: options.launchConfig.agentCommand?.replace(/^qodercli(?=\s|$)/, selected)
}
}
: {})
}
}