mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
fix(agent-launch): round 2: keep request fields for C2, extend main's resolver, one answer per workspace
Problem: the first round still carried request fields and window-parity switches that only the next step sends, added a new shared resolver that also changed window code, and left some background launches using a different runtime answer than the one they spawn with. - Move the agent.launch request fields, their capability and fingerprint handling, and the prompt/Codex parity switches out to the next step; today's host behaviour is kept. - Drop the shared resolver: main's resolveLocalWorkspaceRuntime now finds the project by repo id or source repos, folders borrow their single local repo, and a \\wsl$ path is not refused on a stale distro list. Window code is back to main. - Agents started in an existing worktree use that worktree's answer; the phone's fallback paths spawn with the answer they quoted for, and a runtime that needs repair shows its message instead of a plain shell. - The OpenCode model check looks where the agent will run. - A Windows --shell in a \\wsl$ workspace names the path, not a runtime setting that cannot change it. - The temporary folder rule names its follow-up, STA-10231.
This commit is contained in:
@@ -41,8 +41,8 @@ export type AgentLaunchSurfaceExecution = AgentLaunchExecutionBase & {
|
||||
intent: AgentLaunchIntent
|
||||
surfaces: AgentLaunchSurfaceFactory
|
||||
workspaces?: AgentLaunchWorkspaceFactory
|
||||
/** Settles a terminal agent whatever the chat default says, for a caller whose contract is a
|
||||
* terminal handle or whose surface is a terminal tab (`agent.launch`'s `terminalOnly`). */
|
||||
/** Host-internal, never on the wire: settles a terminal agent whatever the chat default says, for
|
||||
* a caller whose contract is a terminal handle. */
|
||||
terminalOnly?: boolean
|
||||
/** The launch's own delivery: argv only when the typed line carries it, else a paste. */
|
||||
promptPolicy?: undefined
|
||||
|
||||
@@ -61,7 +61,6 @@ import {
|
||||
} from './agent-launch-legacy-host'
|
||||
import {
|
||||
AgentLaunchStructuredSessionRefusedError,
|
||||
terminalLaunchInputs,
|
||||
type AgentLaunchStructuredSurface
|
||||
} from './agent-launch-surface-factories'
|
||||
import type {
|
||||
@@ -94,7 +93,7 @@ export async function executeAgentLaunch(
|
||||
...(execution.callerRendersStructured === false ? { callerRendersStructured: false } : {})
|
||||
},
|
||||
settings: readAgentLaunchModeSettings(runtime),
|
||||
...(execution.terminalOnly || intent.terminalOnly ? { terminalOnly: true } : {}),
|
||||
...(execution.terminalOnly ? { terminalOnly: true } : {}),
|
||||
vocabulary
|
||||
})
|
||||
// The create's startup terminal is this launch's only surface, and the create delivers the text.
|
||||
@@ -320,7 +319,7 @@ async function createSurface(
|
||||
function ignoredStructuredAgentArgsWarning(
|
||||
intent: AgentLaunchIntent
|
||||
): { warning: string } | undefined {
|
||||
return intent.agentArgs === undefined && intent.extraAgentArgs === undefined
|
||||
return intent.agentArgs === undefined
|
||||
? undefined
|
||||
: {
|
||||
warning:
|
||||
@@ -328,6 +327,19 @@ function ignoredStructuredAgentArgsWarning(
|
||||
}
|
||||
}
|
||||
|
||||
/** What every route that builds a terminal agent passes on, so the startup terminal of a new
|
||||
* workspace and the terminal of an existing one start the same agent. */
|
||||
function terminalLaunchInputs(intent: AgentLaunchIntent) {
|
||||
return {
|
||||
...(intent.sessionOptions ? { options: intent.sessionOptions } : {}),
|
||||
// `null` is a value the caller meant, so this tests for absence rather than falsiness.
|
||||
...(intent.agentArgs !== undefined ? { agentArgs: intent.agentArgs } : {}),
|
||||
...(intent.cwd ? { cwd: intent.cwd } : {}),
|
||||
...(intent.launchSource ? { launchSource: intent.launchSource } : {}),
|
||||
...(intent.paneKey ? { paneKey: intent.paneKey } : {})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The one place a terminal agent is created, so the structured-refusal downgrade builds the same
|
||||
* surface — carrying the same argv prompt — as a launch that chose a terminal outright.
|
||||
|
||||
@@ -130,14 +130,13 @@ const HOST_SUPPORT_REASON: Record<
|
||||
export function decideAgentLaunchMode(args: {
|
||||
placement: AgentLaunchModePlacement
|
||||
settings: AgentLaunchModeSettings | null | undefined
|
||||
/** The caller asked for a terminal (its contract is a terminal handle), so the chat default
|
||||
* cannot apply. */
|
||||
/** Host-internal: the caller's contract is a terminal handle, so the chat default cannot apply. */
|
||||
terminalOnly?: boolean
|
||||
vocabulary?: AgentLaunchModeVocabulary
|
||||
}): AgentLaunchModeReceipt {
|
||||
const { placement, settings } = args
|
||||
const vocabulary = args.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY
|
||||
if (!isNativeChatEnabled(settings)) {
|
||||
if (args.terminalOnly || !isNativeChatEnabled(settings)) {
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'terminal',
|
||||
@@ -145,15 +144,6 @@ export function decideAgentLaunchMode(args: {
|
||||
detail: `Started ${vocabulary.terminal}, the default for new agent tabs in your settings.`
|
||||
}
|
||||
}
|
||||
if (args.terminalOnly) {
|
||||
// No reason word says "the caller asked" yet; a new one needs a capability older peers read.
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'terminal',
|
||||
reason: 'user_default',
|
||||
detail: `Started ${vocabulary.terminal} because this launch asked for one.`
|
||||
}
|
||||
}
|
||||
// A worker placed on another runtime starts through federation, which creates terminal agents
|
||||
// only; this host cannot answer for that runtime's structured support.
|
||||
if (placement.on) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* implements, not part of the sequencing it runs.
|
||||
*/
|
||||
|
||||
import type { AgentLaunchIntent, AgentLaunchPrompt } from '../../shared/agent-launch-intent'
|
||||
import type { AgentLaunchPrompt } from '../../shared/agent-launch-intent'
|
||||
import type { StructuredAgentId } from '../../shared/agent-session-provider-handle'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
|
||||
@@ -39,10 +39,6 @@ export type AgentLaunchSurfaceFactory = {
|
||||
paneKey?: string
|
||||
/** The tab's first view, derived on the host by the window's own rule. */
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
/** As on the intent: the tab's title, saved extra arguments, and `agent_started.request_kind`. */
|
||||
title?: AgentLaunchIntent['title']
|
||||
extraAgentArgs?: AgentLaunchIntent['extraAgentArgs']
|
||||
requestKind?: AgentLaunchIntent['requestKind']
|
||||
}): Promise<{
|
||||
handle: string
|
||||
/** The pane this create minted; a factory whose runtime reports none omits it, never invents. */
|
||||
@@ -125,10 +121,6 @@ export type AgentLaunchWorkspaceFactory = {
|
||||
paneKey?: string
|
||||
/** The launch's session options, read as the startup terminal's model/effort/mode preferences. */
|
||||
options?: Readonly<Record<string, unknown>>
|
||||
/** As on the intent: the tab's title, saved extra arguments, and `agent_started.request_kind`. */
|
||||
title?: AgentLaunchIntent['title']
|
||||
extraAgentArgs?: AgentLaunchIntent['extraAgentArgs']
|
||||
requestKind?: AgentLaunchIntent['requestKind']
|
||||
}): Promise<{
|
||||
worktreeId: string
|
||||
/** The new workspace's SSH connection; `null` is local. The executor carries it but nothing
|
||||
@@ -148,19 +140,3 @@ export type AgentLaunchWorkspaceFactory = {
|
||||
create: Readonly<Record<string, unknown>>
|
||||
}): Promise<{ worktreeId: string; connectionId: string | null }>
|
||||
}
|
||||
|
||||
/** What every route that builds a terminal agent passes on, so the startup terminal of a new
|
||||
* workspace and the terminal of an existing one start the same agent. */
|
||||
export function terminalLaunchInputs(intent: AgentLaunchIntent) {
|
||||
return {
|
||||
...(intent.sessionOptions ? { options: intent.sessionOptions } : {}),
|
||||
// `null` is a value the caller meant, so this tests for absence rather than falsiness.
|
||||
...(intent.agentArgs !== undefined ? { agentArgs: intent.agentArgs } : {}),
|
||||
...(intent.cwd ? { cwd: intent.cwd } : {}),
|
||||
...(intent.launchSource ? { launchSource: intent.launchSource } : {}),
|
||||
...(intent.paneKey ? { paneKey: intent.paneKey } : {}),
|
||||
...(intent.title ? { title: intent.title } : {}),
|
||||
...(intent.extraAgentArgs ? { extraAgentArgs: intent.extraAgentArgs } : {}),
|
||||
...(intent.requestKind ? { requestKind: intent.requestKind } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* The `agent_started` triple for an agent launch the host builds; `launch_source` and
|
||||
* `request_kind` come from the caller.
|
||||
* The `agent_started` triple for an agent launch the host builds, of which only `launch_source`
|
||||
* comes from the caller.
|
||||
*
|
||||
* `agent_kind` is derived rather than accepted — a value the host derives is one a caller cannot
|
||||
* misreport. `request_kind` is the caller's (a resume or follow-up the host cannot tell from a fresh
|
||||
* start), read as leniently as `launch_source`: absent or unknown is `new`.
|
||||
* `agent_kind` and `request_kind` are derived rather than accepted — the host already knows both,
|
||||
* and a value it derives is a value a caller cannot misreport. `request_kind` is always `new`
|
||||
* because a host-built launch is always a fresh agent.
|
||||
*
|
||||
* Every launch is attributed: a caller that names no surface, or one this build has never heard of
|
||||
* (a newer client), is recorded as `unknown` rather than dropped. The wire keeps the arm set open so
|
||||
@@ -13,20 +13,18 @@
|
||||
*/
|
||||
|
||||
import { tuiAgentToAgentKind } from '../../shared/agent-kind'
|
||||
import { launchSourceSchema, requestKindSchema } from '../../shared/telemetry-property-schemas'
|
||||
import { launchSourceSchema } from '../../shared/telemetry-property-schemas'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
|
||||
|
||||
export function agentStartedTelemetry(
|
||||
agent: TuiAgent,
|
||||
launchSource: string | undefined,
|
||||
requestKind?: string
|
||||
launchSource: string | undefined
|
||||
): NonNullable<WorktreeStartupLaunch['telemetry']> {
|
||||
const parsed = launchSourceSchema.safeParse(launchSource)
|
||||
const kind = requestKindSchema.safeParse(requestKind)
|
||||
return {
|
||||
agent_kind: tuiAgentToAgentKind(agent),
|
||||
launch_source: parsed.success ? parsed.data : 'unknown',
|
||||
request_kind: kind.success ? kind.data : 'new'
|
||||
request_kind: 'new'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ describe('runtime pty spawn preflight: requested shell on a local Windows host',
|
||||
shellOverride: 'cmd.exe',
|
||||
launchRuntime: {
|
||||
platform: 'linux',
|
||||
remote: false,
|
||||
pty: { shellOverride: 'wsl.exe', terminalWindowsWslDistro: 'Ubuntu' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,10 +5,12 @@ import type { Repo } from '../shared/repo-types'
|
||||
import type { FolderWorkspace } from '../shared/folder-workspace-types'
|
||||
import type { ProjectGroup } from '../shared/project-group-types'
|
||||
import {
|
||||
findLocalFolderWorkspaceRuntimeRepo,
|
||||
resolveLocalWorkspaceExecutionRuntime
|
||||
} from '../shared/local-workspace-execution-runtime'
|
||||
findFolderWorkspaceCandidateRepos,
|
||||
resolveFolderWorkspaceHost,
|
||||
type FolderWorkspaceHostState
|
||||
} from '../shared/folder-workspace-execution-host'
|
||||
import {
|
||||
getWorkspaceRuntimePreference,
|
||||
resolveProjectExecutionRuntime,
|
||||
type ProjectExecutionRuntimeResolution
|
||||
} from '../shared/project-execution-runtime'
|
||||
@@ -19,8 +21,10 @@ import {
|
||||
hasCachedWslDistros
|
||||
} from './wsl'
|
||||
import { getRepoIdFromWorktreeId } from '../shared/worktree/id'
|
||||
import { resolveLocalPtyRuntimeOptions } from '../shared/agent-launch-runtime'
|
||||
import type { LocalWindowsTerminalRuntimeOptions } from '../shared/local-windows-terminal-runtime'
|
||||
import {
|
||||
resolveLocalWindowsTerminalRuntimeOptions,
|
||||
type LocalWindowsTerminalRuntimeOptions
|
||||
} from '../shared/local-windows-terminal-runtime'
|
||||
import { resolveManagedSshHostLoginShell } from './pty/managed-ssh-host-login-shell'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
|
||||
|
||||
@@ -61,27 +65,20 @@ function resolveLocalProjectRuntime(
|
||||
project: Pick<Project, 'id' | 'localWindowsRuntimePreference'>,
|
||||
settings: ReturnType<ResolvableStore['getSettings']> = store.getSettings()
|
||||
): ProjectExecutionRuntimeResolution {
|
||||
const wslAvailable = hasCachedWslAvailability()
|
||||
? (getCachedWslAvailability() ?? undefined)
|
||||
: undefined
|
||||
const availableWslDistros = hasCachedWslDistros() ? getCachedWslDistros() : null
|
||||
return resolveProjectExecutionRuntime({
|
||||
appPlatform: process.platform,
|
||||
projectId: project.id,
|
||||
projectRuntimePreference: project.localWindowsRuntimePreference,
|
||||
globalWindowsRuntimeDefault: settings.localWindowsRuntimeDefault,
|
||||
...cachedWslFacts()
|
||||
wslAvailable,
|
||||
availableWslDistros
|
||||
})
|
||||
}
|
||||
|
||||
function cachedWslFacts(): {
|
||||
wslAvailable?: boolean
|
||||
availableWslDistros: readonly string[] | null
|
||||
} {
|
||||
return {
|
||||
wslAvailable: hasCachedWslAvailability()
|
||||
? (getCachedWslAvailability() ?? undefined)
|
||||
: undefined,
|
||||
availableWslDistros: hasCachedWslDistros() ? getCachedWslDistros() : null
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveLocalProjectRuntimeForRepo(
|
||||
store: ProjectRuntimeResolutionStore,
|
||||
repo: Repo
|
||||
@@ -99,8 +96,8 @@ export function resolveLocalProjectRuntimeForRepo(
|
||||
return resolveLocalProjectRuntime(store, project)
|
||||
}
|
||||
|
||||
/** The window's runtime for a repo workspace (`resolveLocalWorkspaceExecutionRuntime`), read from
|
||||
* main's store and WSL caches. Works for a repo with no project too. */
|
||||
/** Same choice the renderer makes for a workspace: the project's runtime, else the distro of a
|
||||
* WSL-share path, else the global default. Works for a repo with no project too. */
|
||||
export function resolveLocalWorkspaceRuntime(
|
||||
store: ProjectRuntimeResolutionStore,
|
||||
repo: Repo,
|
||||
@@ -112,14 +109,46 @@ export function resolveLocalWorkspaceRuntime(
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return resolveLocalWorkspaceExecutionRuntime({
|
||||
appPlatform: process.platform,
|
||||
projects: store.getProjects(),
|
||||
settings: store.getSettings(),
|
||||
repoId: repo.id,
|
||||
workspacePath,
|
||||
...cachedWslFacts()
|
||||
})
|
||||
const project = store
|
||||
.getProjects()
|
||||
.find((entry) => entry.id === repo.id || entry.sourceRepoIds.includes(repo.id))
|
||||
const preference = getWorkspaceRuntimePreference(
|
||||
project?.localWindowsRuntimePreference,
|
||||
workspacePath
|
||||
)
|
||||
const id = project?.id ?? repo.id
|
||||
// A `\\wsl$` path proves its distro exists, and main's distro cache can be stale; the window
|
||||
// checks such a path against no cache either.
|
||||
return !project?.localWindowsRuntimePreference && preference.kind === 'wsl'
|
||||
? resolveProjectExecutionRuntime({
|
||||
appPlatform: process.platform,
|
||||
projectId: id,
|
||||
projectRuntimePreference: preference,
|
||||
globalWindowsRuntimeDefault: store.getSettings().localWindowsRuntimeDefault
|
||||
})
|
||||
: resolveLocalProjectRuntime(store, { id, localWindowsRuntimePreference: preference })
|
||||
}
|
||||
|
||||
/** The one local repo whose project runs a folder workspace on this machine; none for a folder
|
||||
* pinned to another host or spanning several repos. */
|
||||
export function findLocalFolderWorkspaceRuntimeRepo(
|
||||
state: FolderWorkspaceHostState,
|
||||
folderWorkspaceId: string
|
||||
): Repo | undefined {
|
||||
const folder = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
|
||||
if (!folder || resolveFolderWorkspaceHost(state, folder.id).kind !== 'local') {
|
||||
return undefined
|
||||
}
|
||||
const group = state.projectGroups.find((entry) => entry.id === folder.projectGroupId)
|
||||
if (
|
||||
(folder.executionHostId ?? group?.executionHostId ?? LOCAL_EXECUTION_HOST_ID) !==
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const candidates = findFolderWorkspaceCandidateRepos(state, folder.id)
|
||||
const repo = candidates.length === 1 ? candidates[0] : undefined
|
||||
return repo && getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID ? repo : undefined
|
||||
}
|
||||
|
||||
export type LaunchScopeRuntimeStore = ProjectRuntimeResolutionStore & {
|
||||
@@ -128,8 +157,8 @@ export type LaunchScopeRuntimeStore = ProjectRuntimeResolutionStore & {
|
||||
getProjectGroups?: () => ProjectGroup[]
|
||||
}
|
||||
|
||||
/** The window's runtime for a workspace (`getLocalProjectExecutionRuntimeContext`), for a launch
|
||||
* scope this host resolved. A remote scope has none: its host plans for itself. */
|
||||
/** A launch scope's runtime, as the window reads it for a workspace. A remote scope has none: its
|
||||
* host plans for itself. */
|
||||
export function resolveLocalLaunchScopeRuntime(
|
||||
store: LaunchScopeRuntimeStore,
|
||||
scope: {
|
||||
@@ -207,19 +236,47 @@ export function resolveLocalProjectRuntimeForWorktreeId(
|
||||
return repo ? resolveLocalProjectRuntimeForRepo(store, repo) : undefined
|
||||
}
|
||||
|
||||
/** A plain terminal's PTY on this host: the spawn's rule when no agent plan brought its own. */
|
||||
/** A local PTY's shell and distro. Throws for a runtime that needs repair. */
|
||||
export function resolveLocalPtyOptions(
|
||||
settings:
|
||||
| Partial<
|
||||
Pick<
|
||||
GlobalSettings,
|
||||
'terminalWindowsShell' | 'terminalWindowsWslDistro' | 'terminalDefaultShell'
|
||||
>
|
||||
>
|
||||
| undefined,
|
||||
projectRuntime: ProjectExecutionRuntimeResolution | undefined,
|
||||
requestedShellOverride: string | undefined,
|
||||
/** Replaces a `wsl.exe` setting on a Windows-host runtime; absent is `powershell.exe`. */
|
||||
fallbackHostShell?: string
|
||||
): LocalWindowsTerminalRuntimeOptions {
|
||||
return process.platform === 'win32'
|
||||
? resolveLocalWindowsTerminalRuntimeOptions({
|
||||
requestedShellOverride,
|
||||
settings,
|
||||
projectRuntime,
|
||||
fallbackHostShell,
|
||||
sshLoginShell: resolveManagedSshHostLoginShell()
|
||||
})
|
||||
: {
|
||||
shellOverride: requestedShellOverride ?? (settings?.terminalDefaultShell || undefined),
|
||||
terminalWindowsWslDistro: null
|
||||
}
|
||||
}
|
||||
|
||||
/** A plain terminal's PTY: the project-only runtime and `COMSPEC`, as the spawn picks when no
|
||||
* agent plan brought its own. */
|
||||
export function resolvePlainTerminalPtyOptions(
|
||||
store: ProjectRuntimeResolutionStore | undefined,
|
||||
settings: Parameters<typeof resolveLocalPtyRuntimeOptions>[0]['settings'],
|
||||
settings: Parameters<typeof resolveLocalPtyOptions>[0],
|
||||
worktreeId: string | undefined,
|
||||
requestedShellOverride: string | undefined
|
||||
): LocalWindowsTerminalRuntimeOptions {
|
||||
return resolveLocalPtyRuntimeOptions({
|
||||
hostPlatform: process.platform,
|
||||
requestedShellOverride,
|
||||
return resolveLocalPtyOptions(
|
||||
settings,
|
||||
projectRuntime: resolveLocalProjectRuntimeForWorktreeId(store, worktreeId),
|
||||
fallbackHostShell: process.env.COMSPEC || 'powershell.exe',
|
||||
sshLoginShell: resolveManagedSshHostLoginShell()
|
||||
})
|
||||
resolveLocalProjectRuntimeForWorktreeId(store, worktreeId),
|
||||
requestedShellOverride,
|
||||
process.env.COMSPEC || 'powershell.exe'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from '../../shared/execution-host'
|
||||
import { resolveFolderWorkspaceHost } from '../../shared/folder-workspace-execution-host'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import { findLocalFolderWorkspaceRuntimeRepo } from '../../shared/local-workspace-execution-runtime'
|
||||
import {
|
||||
findLocalFolderWorkspaceRuntimeRepo,
|
||||
resolveLocalWorkspaceRuntime,
|
||||
type LaunchScopeRuntimeStore
|
||||
} from '../local-project-runtime-resolution'
|
||||
|
||||
@@ -21,7 +21,7 @@ const scope = (
|
||||
path: string,
|
||||
kind: { repo?: Repo; folder?: FolderWorkspace }
|
||||
): TerminalWorkspaceLaunchScope => ({
|
||||
id: 'w',
|
||||
id: kind.repo ? 'r' : 'f',
|
||||
path,
|
||||
connectionId: null,
|
||||
repo: kind.repo ?? null,
|
||||
@@ -29,6 +29,7 @@ const scope = (
|
||||
})
|
||||
function store(args: {
|
||||
repos?: Repo[]
|
||||
/** Repo `r`'s project runtime. */
|
||||
project?: unknown
|
||||
global?: unknown
|
||||
folder?: FolderWorkspace
|
||||
@@ -36,17 +37,13 @@ function store(args: {
|
||||
}) {
|
||||
const fake = {
|
||||
getProjects: () => [
|
||||
{
|
||||
id: 'p',
|
||||
sourceRepoIds: ['r'],
|
||||
localWindowsRuntimePreference: args.project
|
||||
}
|
||||
{ id: 'p', sourceRepoIds: ['r'], localWindowsRuntimePreference: args.project }
|
||||
],
|
||||
getSettings: () => ({
|
||||
terminalWindowsShell: args.shell ?? 'cmd.exe',
|
||||
localWindowsRuntimeDefault: args.global
|
||||
}),
|
||||
getRepo: () => args.repos?.[0],
|
||||
getRepo: (id: string) => (id === 'r' ? repo(WIN) : undefined),
|
||||
getRepos: () => args.repos ?? [],
|
||||
getFolderWorkspaces: () => (args.folder ? [args.folder] : []),
|
||||
getProjectGroups: () => [{ id: 'g', name: 'g', parentPath: null, connectionId: null }]
|
||||
@@ -75,13 +72,7 @@ describe("the window's runtime rule, on the host", () => {
|
||||
it.each([
|
||||
['repo, project WSL', store({ project: UBUNTU }), scope(WIN, { repo: repo(WIN) }), 'linux'],
|
||||
[
|
||||
'repo, no saved runtime, \\\\wsl$ path',
|
||||
store({}),
|
||||
scope(SHARE, { repo: repo(SHARE) }),
|
||||
'linux'
|
||||
],
|
||||
[
|
||||
'repo, global default WSL',
|
||||
'repo whose project follows a WSL global default',
|
||||
store({ global: UBUNTU }),
|
||||
scope(WIN, { repo: repo(WIN) }),
|
||||
'linux'
|
||||
@@ -101,8 +92,7 @@ describe("the window's runtime rule, on the host", () => {
|
||||
store({ global: UBUNTU, folder: folder('C:\\work') }),
|
||||
scope('C:\\work', { folder: folder('C:\\work') }),
|
||||
'win32'
|
||||
],
|
||||
['repo-less \\\\wsl$ folder', store({}), scope(SHARE, { folder: folder(SHARE) }), 'linux']
|
||||
]
|
||||
])('%s', (_name, s, launchScope, platform) => {
|
||||
expect(agentLaunchPlatformForScope(s, launchScope)).toBe(platform)
|
||||
})
|
||||
@@ -115,45 +105,48 @@ it("answers from main's WSL cache, which the window may not have read yet", () =
|
||||
resolveAgentLaunchRuntime(
|
||||
store({ project: UBUNTU }),
|
||||
scope(WIN, { repo: repo(WIN) }),
|
||||
undefined,
|
||||
'host-legacy'
|
||||
undefined
|
||||
)
|
||||
).toThrow('wsl-distro-missing')
|
||||
// A `\\wsl$` path proves its distro, so a stale cache never refuses it.
|
||||
expect(
|
||||
resolveAgentLaunchRuntime(store({}), scope(SHARE, { repo: repo(SHARE) }), undefined).platform
|
||||
).toBe('linux')
|
||||
// A repo-less `\\wsl$` folder has no runtime to repair, as in the window: its path places it.
|
||||
expect(
|
||||
resolveAgentLaunchRuntime(
|
||||
store({}),
|
||||
scope(SHARE, { folder: folder(SHARE) }),
|
||||
undefined,
|
||||
'host-legacy'
|
||||
).platform
|
||||
resolveAgentLaunchRuntime(store({}), scope(SHARE, { folder: folder(SHARE) }), undefined)
|
||||
.platform
|
||||
).toBe('linux')
|
||||
})
|
||||
|
||||
// Relied on by C2: the pane's pty:spawn takes the fact as is, for quoting and shell alike.
|
||||
it('quotes for the shell the PTY gets, not a setting the runtime replaced', () => {
|
||||
const windowsHost = store({ project: { kind: 'windows-host' }, shell: 'wsl.exe' })
|
||||
const local = resolveAgentLaunchRuntime(
|
||||
windowsHost,
|
||||
scope(WIN, { repo: repo(WIN) }),
|
||||
undefined,
|
||||
'host-legacy'
|
||||
)
|
||||
const local = resolveAgentLaunchRuntime(windowsHost, scope(WIN, { repo: repo(WIN) }), undefined)
|
||||
expect(local.pty.shellOverride).toBe('powershell.exe')
|
||||
expect(agentLaunchQuotingShell(local)).toBe('powershell')
|
||||
})
|
||||
|
||||
// TEMPORARY until C8, which decides how to model a global default nobody chose.
|
||||
it('keeps a WSL Terminal shell where no project chose a runtime, until C8', () => {
|
||||
const s = store({ global: { kind: 'windows-host' }, shell: 'wsl.exe' })
|
||||
const launchScope = scope(WIN, { repo: repo(WIN) })
|
||||
const legacy = resolveAgentLaunchRuntime(s, launchScope, undefined, 'host-legacy')
|
||||
expect([legacy.platform, legacy.pty.shellOverride, agentLaunchQuotingShell(legacy)]).toEqual([
|
||||
'win32',
|
||||
'wsl.exe',
|
||||
'posix'
|
||||
])
|
||||
expect(resolveAgentLaunchRuntime(s, launchScope, undefined, 'window').pty.shellOverride).toBe(
|
||||
'powershell.exe'
|
||||
)
|
||||
})
|
||||
// TEMPORARY, removed by the WSL-shell default decision (STA-10231). Only folders reach it: main
|
||||
// finds no project runtime for a folder, while every local repo has a project.
|
||||
it.each([['follows the global default'], ['is set to Windows host', { kind: 'windows-host' }]])(
|
||||
"keeps a folder's WSL Terminal shell where its repo's project %s",
|
||||
(_name, project?: unknown) => {
|
||||
const legacy = resolveAgentLaunchRuntime(
|
||||
store({
|
||||
project,
|
||||
global: { kind: 'windows-host' },
|
||||
shell: 'wsl.exe',
|
||||
repos: [repo(WIN)],
|
||||
folder: folder('C:\\work')
|
||||
}),
|
||||
scope('C:\\work', { folder: folder('C:\\work') }),
|
||||
undefined
|
||||
)
|
||||
expect([legacy.platform, legacy.pty.shellOverride, agentLaunchQuotingShell(legacy)]).toEqual([
|
||||
'win32',
|
||||
'wsl.exe',
|
||||
'posix'
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import {
|
||||
deriveAgentLaunchRuntime,
|
||||
agentLaunchQuotingShell,
|
||||
executionRuntimePlatform,
|
||||
remoteExecutionPlatform,
|
||||
type AgentLaunchRuntime
|
||||
} from '../../shared/agent-launch-runtime'
|
||||
import { getRepoSshConnectionId } from '../../shared/execution-host'
|
||||
import type { AgentStartupShell } from '../../shared/tui-agent-startup-shell'
|
||||
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
|
||||
import {
|
||||
resolveLocalLaunchScopeRuntime,
|
||||
resolveLocalProjectRuntimeForWorktreeId,
|
||||
resolveLocalPtyOptions,
|
||||
type LaunchScopeRuntimeStore
|
||||
} from '../local-project-runtime-resolution'
|
||||
import { isWslShellName } from '../../shared/local-windows-terminal-runtime'
|
||||
import { resolveManagedSshHostLoginShell } from '../pty/managed-ssh-host-login-shell'
|
||||
import type { RuntimeStore } from './runtime-store-contract'
|
||||
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
|
||||
|
||||
@@ -34,65 +33,68 @@ export function agentLaunchPlatformForScope(
|
||||
}
|
||||
|
||||
/**
|
||||
* TEMPORARY, removed by C8. `host-legacy` keeps today's host outcome for a Windows scope with no
|
||||
* project runtime of its own whose global default is the Windows host: a `wsl.exe` Terminal shell
|
||||
* still runs it in WSL. An explicit "Windows host" default cannot yet be told apart from the
|
||||
* shipped one. `window` is the "+" button's rule.
|
||||
* The runtime an agent launched here runs in: the window's rule, except where main's host had no
|
||||
* runtime for this scope and the rule answers "Windows host". There it keeps main's spawn in the
|
||||
* Terminal shell, since an explicit Windows-host default can't be told from the shipped one.
|
||||
* TEMPORARY (reachable only for folders): removed by the WSL-shell default decision, STA-10231.
|
||||
*/
|
||||
export type WslShellRule = 'host-legacy' | 'window'
|
||||
export function agentLaunchProjectRuntime(
|
||||
store: LaunchScopeRuntimeStore,
|
||||
scope: TerminalWorkspaceLaunchScope
|
||||
): ProjectExecutionRuntimeResolution | undefined {
|
||||
const runtime = resolveLocalLaunchScopeRuntime(store, scope)
|
||||
return runtime?.status === 'resolved' &&
|
||||
runtime.runtime.kind === 'windows-host' &&
|
||||
!resolveLocalProjectRuntimeForWorktreeId(store, scope.id)
|
||||
? undefined
|
||||
: runtime
|
||||
}
|
||||
|
||||
/** The one runtime fact for an agent launched into this workspace. Throws, as the spawn would,
|
||||
* when the workspace's runtime needs repair. */
|
||||
export function resolveAgentLaunchRuntime(
|
||||
store: AgentLaunchRuntimeStore,
|
||||
scope: TerminalWorkspaceLaunchScope,
|
||||
requestedShellOverride: string | undefined,
|
||||
wslShellRule: WslShellRule
|
||||
requestedShellOverride: string | undefined
|
||||
): AgentLaunchRuntime {
|
||||
const settings = store.getSettings()
|
||||
const projectRuntime = resolveLocalLaunchScopeRuntime(store, scope)
|
||||
const keepsWslShell =
|
||||
wslShellRule === 'host-legacy' &&
|
||||
process.platform === 'win32' &&
|
||||
projectRuntime?.status === 'resolved' &&
|
||||
projectRuntime.runtime.kind === 'windows-host' &&
|
||||
projectRuntime.runtime.reason === 'global-default' &&
|
||||
isWslShellName(settings.terminalWindowsShell) &&
|
||||
!resolveLocalProjectRuntimeForWorktreeId(store, scope.id)
|
||||
return deriveAgentLaunchRuntime({
|
||||
hostPlatform: process.platform,
|
||||
remote: Boolean(scope.connectionId),
|
||||
workspacePath: scope.path,
|
||||
// With no runtime, the spawn runs the Terminal shell, as main's host did for these scopes.
|
||||
projectRuntime: keepsWslShell ? undefined : projectRuntime,
|
||||
requestedShellOverride,
|
||||
settings,
|
||||
sshLoginShell: resolveManagedSshHostLoginShell()
|
||||
})
|
||||
if (scope.connectionId) {
|
||||
return {
|
||||
platform: remoteExecutionPlatform(scope.path),
|
||||
pty: { shellOverride: undefined, terminalWindowsWslDistro: null }
|
||||
}
|
||||
}
|
||||
const projectRuntime = agentLaunchProjectRuntime(store, scope)
|
||||
return {
|
||||
platform: executionRuntimePlatform(projectRuntime, scope.path, process.platform),
|
||||
pty: resolveLocalPtyOptions(store.getSettings(), projectRuntime, requestedShellOverride)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The fact for an agent a new workspace of this repo starts, built before the workspace exists. A
|
||||
* runtime that needs repair returns its error with the platform to quote for, so the create goes on
|
||||
* and the startup fails with that error as its spawn would.
|
||||
* The fact for a startup agent, planned before its terminal is created. A runtime that needs repair
|
||||
* gives its error instead, so a worktree create goes on and the startup fails with that error as its
|
||||
* spawn would; that startup is quoted for the Settings shell.
|
||||
*/
|
||||
export function resolveRepoAgentLaunchRuntime(
|
||||
export function resolveStartupAgentLaunchRuntime(
|
||||
store: AgentLaunchRuntimeStore,
|
||||
repo: Repo
|
||||
): AgentLaunchRuntime | { platform: NodeJS.Platform; repairError: Error } {
|
||||
const scope = {
|
||||
id: repo.id,
|
||||
path: repo.path,
|
||||
connectionId: getRepoSshConnectionId(repo),
|
||||
repo,
|
||||
folderWorkspace: null
|
||||
}
|
||||
scope: TerminalWorkspaceLaunchScope
|
||||
): {
|
||||
platform: NodeJS.Platform
|
||||
shell: AgentStartupShell | undefined
|
||||
launchRuntime: AgentLaunchRuntime | Error
|
||||
} {
|
||||
try {
|
||||
return resolveAgentLaunchRuntime(store, scope, undefined, 'host-legacy')
|
||||
const launchRuntime = resolveAgentLaunchRuntime(store, scope, undefined)
|
||||
return {
|
||||
platform: launchRuntime.platform,
|
||||
shell: agentLaunchQuotingShell(launchRuntime),
|
||||
launchRuntime
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
platform: agentLaunchPlatformForScope(store, scope),
|
||||
repairError: error instanceof Error ? error : new Error(String(error))
|
||||
shell: undefined,
|
||||
launchRuntime: error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../shared/runtime-navigation'
|
||||
import { getRepoExecutionHostId } from '../../shared/execution-host'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import type { RuntimeTerminalWait } from '../../shared/runtime-terminal-contracts'
|
||||
import type {
|
||||
@@ -160,7 +161,7 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM
|
||||
...(requestedAgent ? { requestedAgent } : {}),
|
||||
...(launchSource ? { launchSource } : {}),
|
||||
settings: this.store.getSettings(),
|
||||
getLaunchRuntime: () => this.resolveRepoAgentLaunchRuntime(repo)
|
||||
getLaunchRuntime: () => this.resolveStartupAgentLaunchRuntime(repo)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,8 +174,9 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM
|
||||
agentArgs?: string | null
|
||||
extraAgentArgs?: string
|
||||
launchSource?: string
|
||||
requestKind?: string
|
||||
onPromptCarry?: (carried: boolean) => void
|
||||
/** The existing workspace it starts in; absent is a new workspace of `repo`. */
|
||||
launchScope?: TerminalWorkspaceLaunchScope
|
||||
}
|
||||
): ReturnType<typeof buildWorktreeStartupForAgent> {
|
||||
if (!this.store) {
|
||||
@@ -188,10 +190,10 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM
|
||||
...(launchInputs?.agentArgs !== undefined ? { agentArgs: launchInputs.agentArgs } : {}),
|
||||
...(launchInputs?.extraAgentArgs ? { extraAgentArgs: launchInputs.extraAgentArgs } : {}),
|
||||
...(launchInputs?.launchSource ? { launchSource: launchInputs.launchSource } : {}),
|
||||
...(launchInputs?.requestKind ? { requestKind: launchInputs.requestKind } : {}),
|
||||
...(launchInputs?.onPromptCarry ? { onPromptCarry: launchInputs.onPromptCarry } : {}),
|
||||
settings: this.store.getSettings(),
|
||||
getLaunchRuntime: () => this.resolveRepoAgentLaunchRuntime(repo),
|
||||
getLaunchRuntime: () =>
|
||||
this.resolveStartupAgentLaunchRuntime(repo, launchInputs?.launchScope),
|
||||
toSessionOptions: (preferences) => this.toAgentSessionOptions(preferences)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
|
||||
: null
|
||||
const effectiveStartup = args.startup ?? agentStartup?.startup ?? draftStartup?.startup
|
||||
const effectiveStartupFollowup = agentStartup?.followup
|
||||
// The repo's fact: the startup is planned before the new worktree's path exists.
|
||||
const startupLaunchRuntime = args.startup
|
||||
? undefined
|
||||
: (agentStartup ?? draftStartup)?.launchRuntime
|
||||
|
||||
@@ -19,8 +19,8 @@ import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-termi
|
||||
import { resolveStartupShell, type AgentStartupShell } from '../../shared/tui-agent-startup-shell'
|
||||
import { isTuiAgent } from '../../shared/tui-agent-config'
|
||||
import { resolveConfiguredWorkerAgent } from './orchestration/configured-worker-agent-selector'
|
||||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
import { resolveLocalProjectRuntimeForRepo } from '../project-runtime-git-options'
|
||||
import { resolveAgentLaunchRuntime } from './agent-launch-scope-runtime'
|
||||
import { resolveWslSessionContext } from '../daemon/wsl-session-context'
|
||||
|
||||
import { prepareOpenCodeModelStartupInputs } from '../opencode/opencode-model-startup-plan'
|
||||
import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs'
|
||||
@@ -244,29 +244,24 @@ export class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRuntimeWithAd
|
||||
return false
|
||||
}
|
||||
const store = this.requireStore()
|
||||
const settings = store.getSettings()
|
||||
const path = workspace?.path
|
||||
const unc = path ? parseWslUncPath(path) : null
|
||||
const projectRuntime = executionRepo
|
||||
? resolveLocalProjectRuntimeForRepo(store, executionRepo)
|
||||
: null
|
||||
if (projectRuntime?.status === 'repair-required') {
|
||||
return false
|
||||
}
|
||||
const wsl = unc
|
||||
? { distro: unc.distro }
|
||||
: projectRuntime?.runtime.kind === 'wsl'
|
||||
? { distro: projectRuntime.runtime.distro }
|
||||
: undefined
|
||||
if (!path) {
|
||||
return false
|
||||
}
|
||||
// The launch's own runtime, so the probe checks the machine the agent will run on.
|
||||
let launchRuntime
|
||||
try {
|
||||
launchRuntime = resolveAgentLaunchRuntime(store, workspace, undefined)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const wsl = resolveWslSessionContext({ cwd: path, ...launchRuntime.pty })
|
||||
try {
|
||||
await prepareOpenCodeModelStartupInputs({
|
||||
inputs: resolveAgentStartupPlanInputs({
|
||||
agent: 'opencode',
|
||||
settings,
|
||||
platform: wsl ? 'linux' : process.platform,
|
||||
settings: store.getSettings(),
|
||||
platform: launchRuntime.platform,
|
||||
isRemote: false,
|
||||
// A yes/no model probe; nothing is typed into a shell.
|
||||
shell: undefined,
|
||||
|
||||
@@ -174,14 +174,19 @@ export class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh extends OrcaRu
|
||||
ReturnType<OrcaRuntimeService['resolveMobileSessionTerminalCommand']>
|
||||
> = {}
|
||||
if (tab.launchAgent) {
|
||||
let workspace
|
||||
try {
|
||||
const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
|
||||
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
|
||||
agentStartup = await this.resolveMobileSessionTerminalCommand(workspace, {
|
||||
agent: tab.launchAgent
|
||||
})
|
||||
} catch {
|
||||
// Why: a disabled or unresolvable agent must not make the tab
|
||||
// untappable; fall back to the plain-shell materialize.
|
||||
// untappable; fall back to the plain-shell materialize. A runtime that needs repair
|
||||
// rethrows its message here instead of leaving a plain shell under the agent icon.
|
||||
if (workspace) {
|
||||
this.resolveAgentLaunchRuntime(workspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrcaRuntimeWithGetStructuredAgentSessionCreateSupport } from './orca-ru
|
||||
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
|
||||
import { collectSavedStructuredAgentSessionIds } from './saved-structured-agent-session-restoration'
|
||||
import { seedStructuredAgentSessionTabIndex } from './structured-agent-session-tab-index-seed'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
import { getRepoSshConnectionId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
import type {
|
||||
RuntimeMobileSessionAgentTab,
|
||||
RuntimeMobileSessionTabsSnapshot,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import {
|
||||
agentLaunchPlatformForScope,
|
||||
resolveAgentLaunchRuntime,
|
||||
resolveRepoAgentLaunchRuntime
|
||||
resolveStartupAgentLaunchRuntime
|
||||
} from './agent-launch-scope-runtime'
|
||||
import type { AgentLaunchRuntime } from '../../shared/agent-launch-runtime'
|
||||
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
|
||||
@@ -272,21 +272,24 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu
|
||||
return agentLaunchPlatformForScope(this.requireStore(), scope)
|
||||
}
|
||||
|
||||
protected resolveAgentLaunchRuntime(
|
||||
scope: TerminalWorkspaceLaunchScope,
|
||||
requestedShellOverride?: string
|
||||
): AgentLaunchRuntime {
|
||||
return resolveAgentLaunchRuntime(
|
||||
this.requireStore(),
|
||||
scope,
|
||||
requestedShellOverride,
|
||||
'host-legacy'
|
||||
)
|
||||
protected resolveAgentLaunchRuntime(scope: TerminalWorkspaceLaunchScope): AgentLaunchRuntime {
|
||||
return resolveAgentLaunchRuntime(this.requireStore(), scope, undefined)
|
||||
}
|
||||
|
||||
protected resolveRepoAgentLaunchRuntime(
|
||||
repo: Repo
|
||||
): ReturnType<typeof resolveRepoAgentLaunchRuntime> {
|
||||
return resolveRepoAgentLaunchRuntime(this.requireStore(), repo)
|
||||
/** A startup's fact: the workspace it starts in, else (a create, before it exists) the repo. */
|
||||
protected resolveStartupAgentLaunchRuntime(
|
||||
repo: Repo,
|
||||
scope?: TerminalWorkspaceLaunchScope
|
||||
): ReturnType<typeof resolveStartupAgentLaunchRuntime> {
|
||||
return resolveStartupAgentLaunchRuntime(
|
||||
this.requireStore(),
|
||||
scope ?? {
|
||||
id: repo.id,
|
||||
path: repo.path,
|
||||
connectionId: getRepoSshConnectionId(repo),
|
||||
repo,
|
||||
folderWorkspace: null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ export class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWi
|
||||
targetGroupId: opts.targetGroupId,
|
||||
supportsSplitGroupPlacement: opts.supportsSplitGroupPlacement,
|
||||
launchConfig: startupCommand.launchConfig,
|
||||
launchRuntime: startupCommand.launchRuntime,
|
||||
signal: opts.signal
|
||||
}
|
||||
)
|
||||
|
||||
@@ -166,13 +166,10 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC
|
||||
if (!repo) {
|
||||
throw new Error('Repository for the selected workspace is no longer available.')
|
||||
}
|
||||
const startup = this.buildStartupForAgent(
|
||||
repo,
|
||||
opts.agent,
|
||||
opts.prompt,
|
||||
undefined,
|
||||
opts.extraAgentArgs ? { extraAgentArgs: opts.extraAgentArgs } : undefined
|
||||
)
|
||||
const startup = this.buildStartupForAgent(repo, opts.agent, opts.prompt, undefined, {
|
||||
...(opts.extraAgentArgs ? { extraAgentArgs: opts.extraAgentArgs } : {}),
|
||||
launchScope: await this.resolveTerminalWorkspaceLaunchScope(`id:${worktree.id}`)
|
||||
})
|
||||
if (startup.launchRuntime instanceof Error) {
|
||||
throw startup.launchRuntime
|
||||
}
|
||||
|
||||
@@ -54,11 +54,7 @@ async function agentLaunchIntent(
|
||||
...(params.cwd ? { cwd: params.cwd } : {}),
|
||||
...(params.launchSource ? { launchSource: params.launchSource } : {}),
|
||||
...(paneKey ? { paneKey } : {}),
|
||||
...(params.sessionId ? { sessionId: params.sessionId } : {}),
|
||||
...(params.title ? { title: params.title } : {}),
|
||||
...(params.extraAgentArgs ? { extraAgentArgs: params.extraAgentArgs } : {}),
|
||||
...(params.requestKind ? { requestKind: params.requestKind } : {}),
|
||||
...(params.terminalOnly ? { terminalOnly: true } : {})
|
||||
...(params.sessionId ? { sessionId: params.sessionId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,10 +128,7 @@ export function agentLaunchSurfaceFactory(
|
||||
launchSource,
|
||||
paneKey,
|
||||
options,
|
||||
viewMode,
|
||||
title,
|
||||
extraAgentArgs,
|
||||
requestKind
|
||||
viewMode
|
||||
}) => {
|
||||
const launchPreferences = toAgentLaunchPreferences(options)
|
||||
let promptRodeLaunchCommand = false
|
||||
@@ -160,9 +157,6 @@ export function agentLaunchSurfaceFactory(
|
||||
// A live reserved pane would be attached, not launched into, so the runtime refuses it.
|
||||
...(paneKey ? { ...paneIdentity(paneKey), requireFreshPane: true } : {}),
|
||||
...(launchSource ? { launchSource } : {}),
|
||||
...(requestKind ? { requestKind } : {}),
|
||||
...(title ? { title } : {}),
|
||||
...(extraAgentArgs ? { extraAgentArgs } : {}),
|
||||
...(viewMode ? { viewMode } : {}),
|
||||
...(earlyTab?.windowShowsTab() || !movesHostWindow ? { surfaceOwner: false as const } : {}),
|
||||
onPtySpawnDispatched: terminalSpawn.onPtySpawnDispatched
|
||||
|
||||
@@ -177,8 +177,7 @@ export async function publishAgentLaunchTabEarly(
|
||||
workspacePath: workspace.path,
|
||||
...(params.cwd ? { cwd: params.cwd } : {})
|
||||
},
|
||||
settings,
|
||||
...(params.terminalOnly ? { terminalOnly: true } : {})
|
||||
settings
|
||||
})
|
||||
// A chat's tab is the session's; it appears when the session is created.
|
||||
if (preflight.mode !== 'terminal') {
|
||||
|
||||
@@ -47,10 +47,7 @@ export function agentLaunchWorkspaceFactory(
|
||||
cwd,
|
||||
launchSource,
|
||||
paneKey,
|
||||
options,
|
||||
title,
|
||||
extraAgentArgs,
|
||||
requestKind
|
||||
options
|
||||
}) => {
|
||||
const startupLaunchPreferences = toAgentLaunchPreferences(options)
|
||||
let promptRodeLaunchCommand = false
|
||||
@@ -96,9 +93,6 @@ export function agentLaunchWorkspaceFactory(
|
||||
: {}),
|
||||
...(cwd ? { startupCwd: cwd } : {}),
|
||||
...(launchSource ? { startupLaunchSource: launchSource } : {}),
|
||||
...(requestKind ? { startupRequestKind: requestKind } : {}),
|
||||
...(title ? { startupTitle: title } : {}),
|
||||
...(extraAgentArgs ? { startupExtraAgentArgs: extraAgentArgs } : {}),
|
||||
...(paneKey ? { startupPaneKey: paneKey } : {}),
|
||||
...(startupLaunchPreferences ? { startupLaunchPreferences } : {}),
|
||||
// The launch owns the agent whichever surface it settles on, so the workspace records
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
* error. So `worktree.create` keeps that meaning verbatim, forever, and everything that has to
|
||||
* choose a surface comes here instead, behind a negotiated capability.
|
||||
*
|
||||
* A caller never asks for a chat (`terminalOnly` only rules one out) and must read `outcome.kind`
|
||||
* rather than assume one: the receipt always says which surface ran and why, so a downgrade is never
|
||||
* silent.
|
||||
* A caller therefore never asks for a mode, and must read `outcome.kind` rather than assume one:
|
||||
* the receipt always says which surface ran and why, so a downgrade is never silent.
|
||||
*
|
||||
* A launch is also the one call whose retry is most expensive to get wrong — a lost reply means the
|
||||
* caller cannot tell "never ran" from "ran, answer lost" — so a caller may name the operation with
|
||||
|
||||
@@ -17,13 +17,12 @@ const host = {
|
||||
}
|
||||
const CMD: AgentLaunchRuntime = {
|
||||
platform: 'win32',
|
||||
remote: false,
|
||||
pty: { shellOverride: 'cmd.exe', terminalWindowsWslDistro: null }
|
||||
}
|
||||
const plan = (opts: TerminalCreateOptions, launchRuntime: AgentLaunchRuntime = CMD) =>
|
||||
resolveAgentTerminalCreateOptions(workspace, { ...opts, launchRuntime }, host)
|
||||
|
||||
// The host planner's inputs. Relied on by C2 (pane spawns run this plan), C3 and C7.
|
||||
// The host planner's inputs. Relied on by C2, whose pane spawns run this plan.
|
||||
describe('host agent planner', () => {
|
||||
it("quotes for the fact's shell and hands that same fact to the spawn", async () => {
|
||||
const launch = await plan({ startupAgent: 'claude', agentArgs: '--model opus' })
|
||||
@@ -38,36 +37,6 @@ describe('host agent planner', () => {
|
||||
expect(own.launchRuntime).toBeUndefined()
|
||||
})
|
||||
|
||||
it('merges extra arguments and reports the request kind (C3 resume, C7 automations)', async () => {
|
||||
const launch = await plan({
|
||||
startupAgent: 'claude',
|
||||
agentArgs: null,
|
||||
extraAgentArgs: '--model opus',
|
||||
requestKind: 'resume'
|
||||
})
|
||||
expect(launch.command).toBe('claude "--model" "opus"')
|
||||
expect(launch.telemetry?.request_kind).toBe('resume')
|
||||
})
|
||||
|
||||
it("keeps a long prompt on the command line under the window's rule (C3, C7)", async () => {
|
||||
const prompt = 'x'.repeat(600)
|
||||
const carried: boolean[] = []
|
||||
const opts = { startupAgent: 'claude' as const, startupPrompt: prompt }
|
||||
const onStartupPromptCarry = (value: boolean) => carried.push(value)
|
||||
expect((await plan({ ...opts, onStartupPromptCarry })).command).not.toContain(prompt)
|
||||
expect((await plan({ ...opts, promptCarry: 'argv', onStartupPromptCarry })).command).toContain(
|
||||
prompt
|
||||
)
|
||||
expect(carried).toEqual([false, true])
|
||||
})
|
||||
|
||||
it('makes a promptless Codex wait for the shell only when asked (C8 deletes the choice)', async () => {
|
||||
expect((await plan({ startupAgent: 'codex' })).startupCommandDelivery).toBeUndefined()
|
||||
expect(
|
||||
(await plan({ startupAgent: 'codex', codexNoPromptShellWait: true })).startupCommandDelivery
|
||||
).toBe('shell-ready')
|
||||
})
|
||||
|
||||
it("rewrites Qoder once, probing where the fact's PTY runs (C2 skips the window copy)", async () => {
|
||||
vi.mocked(detectAgentCommandsOnHost).mockClear()
|
||||
const platform = process.platform
|
||||
@@ -77,7 +46,6 @@ describe('host agent planner', () => {
|
||||
{ startupAgent: 'qoder', agentArgs: null },
|
||||
{
|
||||
platform: 'linux',
|
||||
remote: false,
|
||||
pty: { shellOverride: 'wsl.exe', terminalWindowsWslDistro: 'Ubuntu' }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,10 +10,7 @@ import type { RuntimeStore } from './runtime-store-contract'
|
||||
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
|
||||
import type { TerminalCreateOptions } from './runtime-terminal-contracts'
|
||||
import { resolveBareAgentLaunchCommand } from './runtime-agent-launch-resolution'
|
||||
import {
|
||||
buildExecutionHostAgentStartupPlan,
|
||||
planExecutionHostStartupWithPromptCandidate
|
||||
} from '../opencode/opencode-model-startup-plan'
|
||||
import { planExecutionHostStartupWithPromptCandidate } from '../opencode/opencode-model-startup-plan'
|
||||
import { agentPromptRidesLaunchCommand } from '../../shared/tui-agent-startup'
|
||||
import {
|
||||
launchHostProvesAgentInFront,
|
||||
@@ -24,17 +21,16 @@ import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-i
|
||||
import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry'
|
||||
import { terminalShellOverrideRefusal } from './terminal-shell-override-host-support'
|
||||
import {
|
||||
resolveLocalLaunchScopeRuntime,
|
||||
resolveLocalProjectRuntimeForWorktreeId,
|
||||
resolvePlainTerminalPtyOptions
|
||||
} from '../local-project-runtime-resolution'
|
||||
import {
|
||||
agentLaunchPlatformForScope,
|
||||
agentLaunchProjectRuntime,
|
||||
resolveAgentLaunchRuntime
|
||||
} from './agent-launch-scope-runtime'
|
||||
import { resolveQoderTerminalCommand } from './qoder-terminal-command-resolution'
|
||||
import { resolveStartupShell } from '../../shared/tui-agent-startup-shell'
|
||||
import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell'
|
||||
import { resolveWslSessionContext } from '../daemon/wsl-session-context'
|
||||
import { isTuiAgentEnabled } from '../../shared/tui-agent-selection'
|
||||
|
||||
@@ -85,7 +81,7 @@ export async function resolveAgentTerminalCreateOptions(
|
||||
projectRuntime:
|
||||
opts.shellOverride && store
|
||||
? plannedAgent || opts.launchRuntime
|
||||
? resolveLocalLaunchScopeRuntime(store, workspace)
|
||||
? agentLaunchProjectRuntime(store, workspace)
|
||||
: resolveLocalProjectRuntimeForWorktreeId(store, workspace.id)
|
||||
: undefined,
|
||||
cwd: resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path,
|
||||
@@ -112,14 +108,7 @@ export async function resolveAgentTerminalCreateOptions(
|
||||
}
|
||||
const runtime =
|
||||
opts.launchRuntime ??
|
||||
(plannedAgent
|
||||
? resolveAgentLaunchRuntime(
|
||||
store,
|
||||
workspace,
|
||||
opts.shellOverride,
|
||||
opts.wslShellRule ?? 'host-legacy'
|
||||
)
|
||||
: undefined)
|
||||
(plannedAgent ? resolveAgentLaunchRuntime(store, workspace, opts.shellOverride) : undefined)
|
||||
const launch =
|
||||
plannedAgent && runtime
|
||||
? await buildRuntimeAgentTerminalStartupOptions(
|
||||
@@ -157,40 +146,30 @@ async function buildRuntimeAgentTerminalStartupOptions(
|
||||
if (opts.startupPrompt && !agentPromptRidesLaunchCommand(agent)) {
|
||||
throw new Error(`Agent ${agent} does not take a startup prompt on its launch command.`)
|
||||
}
|
||||
const startup = {
|
||||
const { plan: startupPlan, promptCarried } = await planExecutionHostStartupWithPromptCandidate({
|
||||
inputs: resolveAgentStartupPlanInputs({
|
||||
agent,
|
||||
settings,
|
||||
platform: runtime.platform,
|
||||
isRemote,
|
||||
...(opts.agentArgs !== undefined ? { agentArgs: opts.agentArgs } : {}),
|
||||
...(opts.extraAgentArgs ? { extraAgentArgs: opts.extraAgentArgs } : {}),
|
||||
shell: agentLaunchQuotingShell(runtime),
|
||||
sessionOptions: host.toSessionOptions(opts.launchPreferences)
|
||||
}),
|
||||
prompt: opts.startupPrompt ?? '',
|
||||
cwd: resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path,
|
||||
hostIdentity: host.hostIdentity
|
||||
}
|
||||
const { plan: startupPlan, promptCarried } =
|
||||
opts.promptCarry === 'argv'
|
||||
? {
|
||||
plan: await buildExecutionHostAgentStartupPlan(startup),
|
||||
promptCarried: startup.prompt.trim() !== ''
|
||||
}
|
||||
: await planExecutionHostStartupWithPromptCandidate({
|
||||
...startup,
|
||||
host: {
|
||||
shellName: nameLocalTypedLineShell({
|
||||
isRemote,
|
||||
...(runtime.pty.shellOverride ? { shellOverride: runtime.pty.shellOverride } : {})
|
||||
}),
|
||||
provesAgentInFront: launchHostProvesAgentInFront({
|
||||
isRemote,
|
||||
launchPlatform: runtime.platform
|
||||
})
|
||||
}
|
||||
})
|
||||
hostIdentity: host.hostIdentity,
|
||||
host: {
|
||||
shellName: nameLocalTypedLineShell({
|
||||
isRemote,
|
||||
...(runtime.pty.shellOverride ? { shellOverride: runtime.pty.shellOverride } : {})
|
||||
}),
|
||||
provesAgentInFront: launchHostProvesAgentInFront({
|
||||
isRemote,
|
||||
launchPlatform: runtime.platform
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!startupPlan) {
|
||||
// Why: an explicit agent that yields no plan would otherwise spawn a bare
|
||||
// shell that never reaches agent readiness.
|
||||
@@ -202,11 +181,6 @@ async function buildRuntimeAgentTerminalStartupOptions(
|
||||
if (opts.startupPrompt) {
|
||||
opts.onStartupPromptCarry?.(promptCarried)
|
||||
}
|
||||
const startupCommandDelivery =
|
||||
startupPlan.startupCommandDelivery ??
|
||||
(opts.codexNoPromptShellWait && agent === 'codex' && !startup.prompt.trim()
|
||||
? 'shell-ready'
|
||||
: undefined)
|
||||
|
||||
return {
|
||||
...opts,
|
||||
@@ -214,11 +188,9 @@ async function buildRuntimeAgentTerminalStartupOptions(
|
||||
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
||||
launchConfig: startupPlan.launchConfig,
|
||||
launchAgent: agent,
|
||||
startupCommandDelivery,
|
||||
startupCommandDelivery: startupPlan.startupCommandDelivery,
|
||||
// A bare command the user typed stays out of launch accounting, as before.
|
||||
...(opts.startupAgent
|
||||
? { telemetry: agentStartedTelemetry(agent, opts.launchSource, opts.requestKind) }
|
||||
: {})
|
||||
...(opts.startupAgent ? { telemetry: agentStartedTelemetry(agent, opts.launchSource) } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,29 +210,24 @@ function resolveQoderLaunchCommand(
|
||||
shell: resolveStartupShell(remoteExecutionPlatform(workspace.path))
|
||||
})
|
||||
}
|
||||
const settings = store.getSettings()
|
||||
const pty =
|
||||
runtime?.pty ??
|
||||
resolvePlainTerminalPtyOptions(store, settings, workspace.id, launch.shellOverride)
|
||||
// A command its caller quoted runs under the spawn's own rule; its tokens are what is searched.
|
||||
const fact = runtime ?? {
|
||||
platform: agentLaunchPlatformForScope(store, workspace),
|
||||
pty: resolvePlainTerminalPtyOptions(
|
||||
store,
|
||||
store.getSettings(),
|
||||
workspace.id,
|
||||
launch.shellOverride
|
||||
)
|
||||
}
|
||||
// Where the process runs: a `\\wsl$` cwd first, then a WSL shell, as the spawn decides it.
|
||||
const wsl = resolveWslSessionContext({
|
||||
cwd: resolveTerminalStartupCwd(workspace.path, launch.cwd) ?? workspace.path,
|
||||
shellOverride: pty.shellOverride,
|
||||
terminalWindowsWslDistro: pty.terminalWindowsWslDistro
|
||||
shellOverride: fact.pty.shellOverride,
|
||||
terminalWindowsWslDistro: fact.pty.terminalWindowsWslDistro
|
||||
})
|
||||
const platform = runtime?.platform ?? agentLaunchPlatformForScope(store, workspace)
|
||||
return resolveQoderTerminalCommand(launch, {
|
||||
context: wsl ? { wslDistro: wsl.distro } : {},
|
||||
// A caller-quoted command was quoted by the Settings shell, as the window quotes.
|
||||
shell: runtime
|
||||
? agentLaunchQuotingShell(runtime)
|
||||
: resolveStartupShell(
|
||||
platform,
|
||||
resolveLocalWindowsAgentStartupShell({
|
||||
platform,
|
||||
isRemote: false,
|
||||
terminalWindowsShell: launch.shellOverride ?? settings.terminalWindowsShell
|
||||
})
|
||||
)
|
||||
shell: agentLaunchQuotingShell(fact)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ export async function createRuntimeFolderWorktree(args: {
|
||||
const terminal = await deps.createTerminal(`id:${worktree.id}`, {
|
||||
command: args.startup.command,
|
||||
...(request.startupCwd ? { cwd: request.startupCwd } : {}),
|
||||
...(request.startupTitle ? { title: request.startupTitle } : {}),
|
||||
...paneIdentity(request.startupPaneKey),
|
||||
env: args.startup.env,
|
||||
...(args.startup.launchConfig ? { launchConfig: args.startup.launchConfig } : {}),
|
||||
|
||||
@@ -104,7 +104,6 @@ export async function startRuntimeLocalWorktreeTerminals(args: {
|
||||
const terminal = await ports.createTerminal(`id:${worktree.id}`, {
|
||||
command: sequencedStartup.command,
|
||||
...(request.startupCwd ? { cwd: request.startupCwd } : {}),
|
||||
...(request.startupTitle ? { title: request.startupTitle } : {}),
|
||||
...paneIdentity(request.startupPaneKey),
|
||||
...(setup && startup ? { claudeAgentTeamsSourceCommand: startup.command } : {}),
|
||||
env: sequencedStartup.env,
|
||||
|
||||
@@ -61,12 +61,8 @@ export type RuntimeManagedWorktreeCreateArgs = {
|
||||
/** Main-internal: an automation's saved extras, merged over the startup agent's arguments. */
|
||||
startupExtraAgentArgs?: string
|
||||
startupCwd?: string
|
||||
/** The startup terminal's tab title. */
|
||||
startupTitle?: string
|
||||
/** The surface behind a host-built startup agent (`startupAgent` or `startupDraft`). */
|
||||
startupLaunchSource?: string
|
||||
/** `agent_started.request_kind` for a host-built `startupAgent`; absent is `new`. */
|
||||
startupRequestKind?: string
|
||||
/** A caller-minted `tabId:leafId` for the startup terminal's pane. */
|
||||
startupPaneKey?: string
|
||||
pendingFirstAgentMessageRename?: boolean
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AgentLaunchRuntime } from '../../shared/agent-launch-runtime'
|
||||
import type { WslShellRule } from './agent-launch-scope-runtime'
|
||||
import type { SpawnIntent } from '../../shared/host-spawn-admission'
|
||||
import type { ParsedAgentStatusPayload } from '../../shared/agent-status-types'
|
||||
import type {
|
||||
@@ -66,22 +65,6 @@ export type TerminalCreateOptions = {
|
||||
* the runtime still builds, so overriding them does not take the launch away from it.
|
||||
*/
|
||||
agentArgs?: string | null
|
||||
/** An automation's saved extras, merged over the launch's arguments; invalid extras throw. */
|
||||
extraAgentArgs?: string
|
||||
/** `agent_started.request_kind` for a `startupAgent` launch; absent or unknown is `new`. */
|
||||
requestKind?: string
|
||||
/**
|
||||
* Launch-convergence parity: how a `startupPrompt` this line cannot carry is handled. Absent is
|
||||
* the host's rule (start clean, the caller pastes it); `argv` is the window's rule (always on the
|
||||
* command line, where long lines are staged). Dies with prompt-at-start.
|
||||
*/
|
||||
promptCarry?: 'argv'
|
||||
/** Launch-convergence parity: a promptless Codex waits for the shell, as window launches do.
|
||||
* Absent is the host's no-wait. Dies when one rule is chosen. */
|
||||
codexNoPromptShellWait?: boolean
|
||||
/** Launch-convergence parity: absent is `host-legacy`, today's host WSL-shell outcome. Dies in
|
||||
* C8 (`WslShellRule`). */
|
||||
wslShellRule?: WslShellRule
|
||||
/** Main-internal: the runtime fact this agent's command was quoted for; its spawn takes the PTY
|
||||
* shell from it. Set by the host planner or a host caller that quoted with it, never a client. */
|
||||
launchRuntime?: AgentLaunchRuntime
|
||||
|
||||
@@ -33,11 +33,11 @@ function makeRepo(fields: Partial<Repo>): Repo {
|
||||
}
|
||||
}
|
||||
|
||||
const LINUX: AgentLaunchRuntime = {
|
||||
const LINUX_FACT: AgentLaunchRuntime = {
|
||||
platform: 'linux',
|
||||
remote: false,
|
||||
pty: { shellOverride: undefined, terminalWindowsWslDistro: null }
|
||||
}
|
||||
const LINUX = { platform: 'linux' as const, shell: 'posix' as const, launchRuntime: LINUX_FACT }
|
||||
|
||||
const settings = {
|
||||
...getDefaultSettings('/tmp'),
|
||||
@@ -123,7 +123,6 @@ describe('buildWorktreeStartupForAgent host resolution', () => {
|
||||
it('quotes a worktree startup for its fact, not the Settings shell, and hands the fact on', () => {
|
||||
const powershell: AgentLaunchRuntime = {
|
||||
platform: 'win32',
|
||||
remote: false,
|
||||
pty: { shellOverride: 'powershell.exe', terminalWindowsWslDistro: null }
|
||||
}
|
||||
const result = buildWorktreeStartupForAgent({
|
||||
@@ -131,7 +130,11 @@ it('quotes a worktree startup for its fact, not the Settings shell, and hands th
|
||||
settings: { ...settings, terminalWindowsShell: 'wsl.exe' },
|
||||
agent: 'claude',
|
||||
prompt: "fix Bob's bug",
|
||||
getLaunchRuntime: () => powershell,
|
||||
getLaunchRuntime: () => ({
|
||||
platform: 'win32' as const,
|
||||
shell: 'powershell' as const,
|
||||
launchRuntime: powershell
|
||||
}),
|
||||
toSessionOptions: () => undefined
|
||||
})
|
||||
expect(result.startup.command).toContain("'fix Bob''s bug'")
|
||||
@@ -147,7 +150,10 @@ describe('buildWorktreeStartupForAgent prompt carry', () => {
|
||||
prompt: 'summarize the diff\nthen list the risks',
|
||||
getLaunchRuntime: () => ({
|
||||
...LINUX,
|
||||
pty: { shellOverride: terminalDefaultShell, terminalWindowsWslDistro: null }
|
||||
launchRuntime: {
|
||||
...LINUX_FACT,
|
||||
pty: { shellOverride: terminalDefaultShell, terminalWindowsWslDistro: null }
|
||||
}
|
||||
}),
|
||||
toSessionOptions: () => undefined,
|
||||
...(onPromptCarry ? { onPromptCarry } : {})
|
||||
@@ -295,7 +301,7 @@ describe('buildWorktreeStartupForAgent extra agent args', () => {
|
||||
const build = vi.fn(() => ({
|
||||
agent: 'claude' as const,
|
||||
startup: { command: 'claude' },
|
||||
launchRuntime: LINUX
|
||||
launchRuntime: LINUX_FACT
|
||||
}))
|
||||
const createArgs: RuntimeManagedWorktreeCreateArgs = {
|
||||
repoSelector: 'repo-1',
|
||||
|
||||
@@ -8,7 +8,8 @@ import { getRepoSshConnectionId } from '../../shared/execution-host'
|
||||
import { isTuiAgent } from '../../shared/tui-agent-config'
|
||||
import { isTuiAgentEnabled, pickTuiAgent } from '../../shared/tui-agent-selection'
|
||||
import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs'
|
||||
import { agentLaunchQuotingShell, type AgentLaunchRuntime } from '../../shared/agent-launch-runtime'
|
||||
import type { AgentLaunchRuntime } from '../../shared/agent-launch-runtime'
|
||||
import type { AgentStartupShell } from '../../shared/tui-agent-startup-shell'
|
||||
import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '../../shared/tui-agent-startup'
|
||||
import { planStartupWithPromptCandidate } from '../../shared/startup-line-prompt-carry'
|
||||
import {
|
||||
@@ -30,21 +31,19 @@ type AttributedWorktreeStartupLaunch = WorktreeStartupLaunch & {
|
||||
type StartupEnvironment = {
|
||||
repo: Repo
|
||||
settings: ReturnType<RuntimeStore['getSettings']>
|
||||
/** Where the agent runs; its command is quoted for this and the create spawns with it. A runtime
|
||||
* that needs repair gives its error instead, which the startup spawn then fails with. */
|
||||
getLaunchRuntime: () => AgentLaunchRuntime | { platform: NodeJS.Platform; repairError: Error }
|
||||
/** Where the agent runs (`resolveStartupAgentLaunchRuntime`): what its command is quoted for, and
|
||||
* the fact the create spawns with, or the repair error its startup fails with. */
|
||||
getLaunchRuntime: () => {
|
||||
platform: NodeJS.Platform
|
||||
shell: AgentStartupShell | undefined
|
||||
launchRuntime: AgentLaunchRuntime | Error
|
||||
}
|
||||
/** Replaces the configured arguments for this launch; `null` means none. */
|
||||
agentArgs?: string | null
|
||||
/** An automation's saved extras, merged over the launch's arguments. */
|
||||
extraAgentArgs?: string
|
||||
/** Caller-supplied telemetry attribution, validated leniently at the host boundary. */
|
||||
launchSource?: string
|
||||
requestKind?: string
|
||||
}
|
||||
|
||||
/** A startup that will fail with its repair error is quoted for the Settings shell, as before. */
|
||||
function startupQuotingShell(launchRuntime: AgentLaunchRuntime | Error) {
|
||||
return launchRuntime instanceof Error ? undefined : agentLaunchQuotingShell(launchRuntime)
|
||||
}
|
||||
|
||||
/** The agent a linked draft starts: the requested one, else the default, else one detected on the
|
||||
@@ -94,13 +93,12 @@ export async function buildWorktreeStartupForDraft(
|
||||
return null
|
||||
}
|
||||
|
||||
const resolved = environment.getLaunchRuntime()
|
||||
const launchRuntime = 'repairError' in resolved ? resolved.repairError : resolved
|
||||
const { platform, shell, launchRuntime } = environment.getLaunchRuntime()
|
||||
const launchArgs = resolveAgentStartupPlanInputs({
|
||||
agent,
|
||||
settings,
|
||||
platform: resolved.platform,
|
||||
shell: startupQuotingShell(launchRuntime),
|
||||
platform,
|
||||
shell,
|
||||
isRemote: repoIsRemote(repo),
|
||||
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {})
|
||||
})
|
||||
@@ -167,13 +165,12 @@ export function buildWorktreeStartupForAgent(
|
||||
if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) {
|
||||
throw new Error('Selected agent is disabled. Choose an enabled agent before creating.')
|
||||
}
|
||||
const resolved = environment.getLaunchRuntime()
|
||||
const launchRuntime = 'repairError' in resolved ? resolved.repairError : resolved
|
||||
const { platform, shell, launchRuntime } = environment.getLaunchRuntime()
|
||||
const planInputs = resolveAgentStartupPlanInputs({
|
||||
agent,
|
||||
settings,
|
||||
platform: resolved.platform,
|
||||
shell: startupQuotingShell(launchRuntime),
|
||||
platform,
|
||||
shell,
|
||||
isRemote: repoIsRemote(repo),
|
||||
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {}),
|
||||
...(environment.extraAgentArgs ? { extraAgentArgs: environment.extraAgentArgs } : {}),
|
||||
@@ -191,7 +188,7 @@ export function buildWorktreeStartupForAgent(
|
||||
}),
|
||||
provesAgentInFront: launchHostProvesAgentInFront({
|
||||
isRemote: repoIsRemote(repo),
|
||||
launchPlatform: resolved.platform
|
||||
launchPlatform: platform
|
||||
})
|
||||
})
|
||||
startupPlan = offered.plan
|
||||
@@ -213,7 +210,7 @@ export function buildWorktreeStartupForAgent(
|
||||
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
|
||||
: {}),
|
||||
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
||||
telemetry: agentStartedTelemetry(agent, environment.launchSource, environment.requestKind)
|
||||
telemetry: agentStartedTelemetry(agent, environment.launchSource)
|
||||
},
|
||||
launchRuntime,
|
||||
...(startupPlan.followupPrompt
|
||||
@@ -237,7 +234,6 @@ export function resolveWorktreeCreateAgentStartup(
|
||||
agentArgs?: string | null
|
||||
extraAgentArgs?: string
|
||||
launchSource?: string
|
||||
requestKind?: string
|
||||
onPromptCarry?: (carried: boolean) => void
|
||||
}
|
||||
) => {
|
||||
@@ -254,7 +250,6 @@ export function resolveWorktreeCreateAgentStartup(
|
||||
...(args.startupAgentArgs !== undefined ? { agentArgs: args.startupAgentArgs } : {}),
|
||||
...(args.startupExtraAgentArgs ? { extraAgentArgs: args.startupExtraAgentArgs } : {}),
|
||||
...(args.startupLaunchSource ? { launchSource: args.startupLaunchSource } : {}),
|
||||
...(args.startupRequestKind ? { requestKind: args.startupRequestKind } : {}),
|
||||
...(args.onStartupPromptCarry ? { onPromptCarry: args.onStartupPromptCarry } : {})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,9 +32,10 @@ export function terminalShellOverrideRefusal(args: {
|
||||
`--shell ${args.shellOverride} names a Windows shell, and this execution host is ${args.platform}, which spawns the user's login shell. No terminal was created; omit --shell.`
|
||||
)
|
||||
}
|
||||
// The path first: no runtime setting can move a `\\wsl$` path out of WSL, so only it names a fix.
|
||||
return (
|
||||
projectRuntimeShellConflict(args.shellOverride, args.projectRuntime) ??
|
||||
wslUncPathShellConflict(args.shellOverride, args.cwd, args.workspacePath)
|
||||
wslUncPathShellConflict(args.shellOverride, args.cwd, args.workspacePath) ??
|
||||
projectRuntimeShellConflict(args.shellOverride, args.projectRuntime)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,15 +60,9 @@ function projectRuntimeShellConflict(
|
||||
return null
|
||||
}
|
||||
// A workspace with no saved runtime of its own follows the default, so name that.
|
||||
const where = runsInWsl ? 'in WSL' : 'on the Windows host'
|
||||
const runs =
|
||||
projectRuntime.runtime.reason === 'global-default'
|
||||
? `This workspace runs its terminals ${where}, the default in Settings > Project Runtime`
|
||||
: `This workspace's project runs its terminals ${where}`
|
||||
const change =
|
||||
projectRuntime.runtime.reason === 'global-default'
|
||||
? 'that default'
|
||||
: "the project's execution runtime"
|
||||
const byDefault = projectRuntime.runtime.reason === 'global-default'
|
||||
const runs = `${byDefault ? 'This workspace runs' : "This workspace's project runs"} its terminals ${runsInWsl ? 'in WSL' : 'on the Windows host'}${byDefault ? ', the default in Settings > Project Runtime' : ''}`
|
||||
const change = byDefault ? 'that default' : "the project's execution runtime"
|
||||
return new Error(
|
||||
runsInWsl
|
||||
? `${runs}, so --shell ${shellOverride} cannot be applied. No terminal was created. Use --shell wsl.exe, or change ${change}.`
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
type ParsedExecutionHost
|
||||
} from '../../../shared/execution-host'
|
||||
import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime'
|
||||
import { executionRuntimePlatform } from '../../../shared/agent-launch-runtime'
|
||||
import { lastVerifiedRuntimeStatus } from '../../../shared/runtime-host-status'
|
||||
import type { AgentStartupShell } from '../../../shared/tui-agent-startup-shell'
|
||||
import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell'
|
||||
@@ -53,6 +52,19 @@ const RUNTIME_PLATFORM_UPDATE =
|
||||
'This Orca server does not report its operating system. Update the server to launch agents on it.'
|
||||
const HOST_UNRESOLVED_WITHHELD = 'Orca cannot tell which host owns this workspace yet.'
|
||||
|
||||
function resolveLocalPlatform(
|
||||
projectRuntime: ProjectExecutionRuntimeResolution | undefined,
|
||||
workspacePath: string | null | undefined
|
||||
): NodeJS.Platform {
|
||||
if (projectRuntime?.status === 'repair-required') {
|
||||
return projectRuntime.repair.preferredRuntime.kind === 'wsl' ? 'linux' : CLIENT_PLATFORM
|
||||
}
|
||||
if (projectRuntime?.status === 'resolved') {
|
||||
return projectRuntime.runtime.kind === 'wsl' ? 'linux' : CLIENT_PLATFORM
|
||||
}
|
||||
return workspacePath && isWslUncPath(workspacePath) ? 'linux' : CLIENT_PLATFORM
|
||||
}
|
||||
|
||||
function resolveHostPlatform(
|
||||
state: HostFactsState,
|
||||
args: {
|
||||
@@ -70,11 +82,7 @@ function resolveHostPlatform(
|
||||
return { kind: 'withheld', reason: HOST_UNRESOLVED_WITHHELD }
|
||||
}
|
||||
if (host.kind === 'local') {
|
||||
const platform = executionRuntimePlatform(
|
||||
args.projectRuntime(),
|
||||
args.workspacePath,
|
||||
CLIENT_PLATFORM
|
||||
)
|
||||
const platform = resolveLocalPlatform(args.projectRuntime(), args.workspacePath)
|
||||
return { kind: 'known', platform, hostKind: 'local' }
|
||||
}
|
||||
const sshTargetId = host.kind === 'ssh' ? host.targetId : args.nestedSshTargetId
|
||||
|
||||
@@ -3,11 +3,11 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import { parseWslUncPath } from '../../../shared/wsl-paths'
|
||||
import {
|
||||
deriveGlobalWindowsRuntimeDefaultFromLegacySettings,
|
||||
getWorkspaceRuntimePreference,
|
||||
resolveProjectExecutionRuntime,
|
||||
type ProjectExecutionRuntimeResolution
|
||||
} from '../../../shared/project-execution-runtime'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
||||
import { resolveLocalWorkspaceExecutionRuntime } from '../../../shared/local-workspace-execution-runtime'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
@@ -80,14 +80,22 @@ export function getLocalProjectExecutionRuntimeContext(
|
||||
if (!isLocalRuntimeRepo(repo) || !isLocalRuntimeWorktree(worktree)) {
|
||||
return undefined
|
||||
}
|
||||
return resolveLocalWorkspaceExecutionRuntime({
|
||||
const projectId = getLocalPreflightProjectId(state, worktreeId)
|
||||
const project = getLocalRuntimeProject(state, projectId, repo.id)
|
||||
const projectRuntimePreference = getWorkspaceRuntimePreference(
|
||||
project?.localWindowsRuntimePreference,
|
||||
worktree?.path ?? repo?.path
|
||||
)
|
||||
|
||||
return resolveProjectExecutionRuntime({
|
||||
appPlatform,
|
||||
projects: state.projects,
|
||||
settings: state.settings,
|
||||
repoId: repo.id,
|
||||
projectId: getLocalPreflightProjectId(state, worktreeId),
|
||||
workspacePath: worktree?.path ?? repo?.path,
|
||||
...wslContext
|
||||
projectId,
|
||||
projectRuntimePreference,
|
||||
globalWindowsRuntimeDefault:
|
||||
state.settings?.localWindowsRuntimeDefault ??
|
||||
deriveGlobalWindowsRuntimeDefaultFromLegacySettings(state.settings).defaultRuntime,
|
||||
wslAvailable: wslContext.wslAvailable,
|
||||
availableWslDistros: wslContext.availableWslDistros
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,13 +138,22 @@ export function getLocalRepoProjectExecutionRuntimeContext(
|
||||
if (!isLocalRuntimeRepo(repo)) {
|
||||
return undefined
|
||||
}
|
||||
return resolveLocalWorkspaceExecutionRuntime({
|
||||
const project = getLocalRuntimeProject(state, repoId, repo.id)
|
||||
const projectId = project?.id ?? repoId
|
||||
const projectRuntimePreference = getWorkspaceRuntimePreference(
|
||||
project?.localWindowsRuntimePreference,
|
||||
repo?.path
|
||||
)
|
||||
|
||||
return resolveProjectExecutionRuntime({
|
||||
appPlatform,
|
||||
projects: state.projects,
|
||||
settings: state.settings,
|
||||
repoId: repo.id,
|
||||
workspacePath: repo.path,
|
||||
...wslContext
|
||||
projectId,
|
||||
projectRuntimePreference,
|
||||
globalWindowsRuntimeDefault:
|
||||
state.settings?.localWindowsRuntimeDefault ??
|
||||
deriveGlobalWindowsRuntimeDefaultFromLegacySettings(state.settings).defaultRuntime,
|
||||
wslAvailable: wslContext.wslAvailable,
|
||||
availableWslDistros: wslContext.availableWslDistros
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,6 +312,17 @@ function isLocalRuntimeWorktree(worktree?: Pick<Worktree, 'hostId'> | null): boo
|
||||
return !worktree?.hostId || worktree.hostId === LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
|
||||
function getLocalRuntimeProject(
|
||||
state: LocalProjectRuntimeState,
|
||||
projectId: string,
|
||||
repoId: string
|
||||
) {
|
||||
return state.projects?.find(
|
||||
(entry) =>
|
||||
entry.id === projectId || entry.id === repoId || entry.sourceRepoIds?.includes(repoId)
|
||||
)
|
||||
}
|
||||
|
||||
function getLocalPreflightProjectId(
|
||||
state: LocalProjectRuntimeState,
|
||||
worktreeId?: string | null
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import { getIndexedWorktreeById } from '@/store/worktree-repo-index'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
||||
import { findLocalFolderWorkspaceRuntimeRepo } from '../../../shared/local-workspace-execution-runtime'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
||||
import {
|
||||
findFolderWorkspaceCandidateRepos,
|
||||
resolveFolderWorkspaceHost
|
||||
} from '../../../shared/folder-workspace-execution-host'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
|
||||
@@ -30,9 +33,20 @@ export function getLocalProjectRuntimeWorkspace(
|
||||
projectGroups: state.projectGroups ?? [],
|
||||
repos: state.repos
|
||||
}
|
||||
const repo = findLocalFolderWorkspaceRuntimeRepo(folderState, scope.folderWorkspaceId)
|
||||
const folder = folderState.folderWorkspaces.find((entry) => entry.id === scope.folderWorkspaceId)
|
||||
return repo && folder
|
||||
if (!folder || resolveFolderWorkspaceHost(folderState, folder.id).kind !== 'local') {
|
||||
return null
|
||||
}
|
||||
const group = folderState.projectGroups.find((entry) => entry.id === folder.projectGroupId)
|
||||
if (
|
||||
(folder.executionHostId ?? group?.executionHostId ?? LOCAL_EXECUTION_HOST_ID) !==
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const candidates = findFolderWorkspaceCandidateRepos(folderState, folder.id)
|
||||
const repo = candidates.length === 1 ? candidates[0] : undefined
|
||||
return repo && getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID
|
||||
? {
|
||||
id: targetWorktreeId,
|
||||
repoId: repo.id,
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
* resolves it once — settings default plus per-launch feasibility from
|
||||
* `structured-native-chat-launch-route` — so no surface carries its own copy of that decision.
|
||||
*
|
||||
* The intent deliberately does NOT pick a chat. A caller states what it wants to happen, not how
|
||||
* The intent deliberately does NOT name a mode. A caller states what it wants to happen, not how
|
||||
* to deliver it; picking structured vs terminal is the host's job and is reported back in the
|
||||
* receipt. `terminalOnly` only rules a chat out, for a caller whose surface is a terminal tab.
|
||||
* receipt rather than requested here.
|
||||
*/
|
||||
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
@@ -98,13 +98,6 @@ export type AgentLaunchIntent = {
|
||||
/** The caller-minted id of the chat session a structured launch creates. Not a route input;
|
||||
* refused when that session already exists. */
|
||||
sessionId?: string
|
||||
/** A terminal launch's tab title. */
|
||||
title?: string
|
||||
/** Saved extras merged over the launch's arguments; terminal-only, like `agentArgs`. */
|
||||
extraAgentArgs?: string
|
||||
/** `agent_started.request_kind`, as leniently as `launchSource`. */
|
||||
requestKind?: string
|
||||
terminalOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,11 +29,7 @@ const OTHER_PANE_KEY = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d:6fa459ea-ee8a-4ca4-
|
||||
* not merely that the input type has no name for it.
|
||||
*/
|
||||
function fingerprintOfWirePayload(
|
||||
params: AgentLaunchFingerprintInput & {
|
||||
launchSource?: string
|
||||
title?: string
|
||||
requestKind?: string
|
||||
}
|
||||
params: AgentLaunchFingerprintInput & { launchSource?: string }
|
||||
): string {
|
||||
return computeAgentLaunchFingerprint(params)
|
||||
}
|
||||
@@ -84,14 +80,6 @@ describe('fields the launch fingerprint deliberately ignores', () => {
|
||||
computeAgentLaunchFingerprint(BASE)
|
||||
)
|
||||
})
|
||||
|
||||
// Relied on by C2/C3/C7, which send these through agent.launchReplay.
|
||||
it('ignores the tab title and request kind, but not the inputs that change what runs', () => {
|
||||
const plain = fingerprintOfWirePayload(BASE)
|
||||
expect(fingerprintOfWirePayload({ ...BASE, title: 'Fix', requestKind: 'resume' })).toBe(plain)
|
||||
expect(fingerprintOfWirePayload({ ...BASE, extraAgentArgs: '--verbose' })).not.toBe(plain)
|
||||
expect(fingerprintOfWirePayload({ ...BASE, terminalOnly: true })).not.toBe(plain)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compatibility with rows written before these fields existed', () => {
|
||||
|
||||
@@ -48,10 +48,6 @@ export type AgentLaunchFingerprintInput = {
|
||||
/** In: a retry that minted another session is a different request, since replaying would answer
|
||||
* with a conversation this caller did not mint. */
|
||||
sessionId?: string
|
||||
/** In: they change the command, as `agentArgs` does. */
|
||||
extraAgentArgs?: string
|
||||
/** In: it decides the surface, as `cwd` can. */
|
||||
terminalOnly?: boolean
|
||||
/**
|
||||
* `launchSource` is deliberately absent, and this is the reasoned exclusion rather than an
|
||||
* oversight: it is telemetry, so two launches differing only in which button produced them do the
|
||||
@@ -60,9 +56,8 @@ export type AgentLaunchFingerprintInput = {
|
||||
* digest covers what the call DOES — and the cost of leaving it out is only that a replay reports
|
||||
* the first attempt's attribution, which is the truthful answer: one launch happened.
|
||||
*
|
||||
* `placement`, `presentation` and `title` are absent by the same rule: they say where the tab sits,
|
||||
* whose view moves and what it is called, not what runs, so a retry that changed them replays the
|
||||
* first answer. `requestKind` is telemetry, excluded as `launchSource` is.
|
||||
* `placement` and `presentation` are absent by the same rule: they say where the tab sits and whose
|
||||
* view moves, not what runs, so a retry that moved them replays the first answer.
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -80,9 +75,7 @@ export function computeAgentLaunchFingerprint(input: AgentLaunchFingerprintInput
|
||||
cwd: input.cwd,
|
||||
// Absent keys are dropped by the canonical form, so every digest without one is unchanged.
|
||||
paneKey: input.paneKey,
|
||||
sessionId: input.sessionId,
|
||||
extraAgentArgs: input.extraAgentArgs,
|
||||
terminalOnly: input.terminalOnly
|
||||
sessionId: input.sessionId
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,6 @@ export const AGENT_LAUNCH_PLACEMENT_RUNTIME_CAPABILITY = 'agent.launch.placement
|
||||
export const AGENT_LAUNCH_CREATE_FOLDER_WORKSPACE_RUNTIME_CAPABILITY =
|
||||
'agent.launch.create-folder-workspace.v1' as const
|
||||
|
||||
// Host-advertised: reads `title`, `extraAgentArgs`, `requestKind` and `terminalOnly`. An older host
|
||||
// drops unknown keys, so a client sends them only to a host that advertises this.
|
||||
export const AGENT_LAUNCH_TERMINAL_INPUTS_RUNTIME_CAPABILITY =
|
||||
'agent.launch.terminal-inputs.v1' as const
|
||||
|
||||
// Client-advertised only: the client reads a listed launch tab with no terminal yet as "not
|
||||
// started", never as proof its agent runs. The host publishes a paired caller's tab before the
|
||||
// spawn only for a client that says so.
|
||||
@@ -67,6 +62,5 @@ export const AGENT_LAUNCH_RUNTIME_CAPABILITIES = [
|
||||
AGENT_LAUNCH_PROMPT_CARRY_RUNTIME_CAPABILITY,
|
||||
AGENT_LAUNCH_PROMPT_UNCONFIRMED_RUNTIME_CAPABILITY,
|
||||
AGENT_LAUNCH_PLACEMENT_RUNTIME_CAPABILITY,
|
||||
AGENT_LAUNCH_CREATE_FOLDER_WORKSPACE_RUNTIME_CAPABILITY,
|
||||
AGENT_LAUNCH_TERMINAL_INPUTS_RUNTIME_CAPABILITY
|
||||
AGENT_LAUNCH_CREATE_FOLDER_WORKSPACE_RUNTIME_CAPABILITY
|
||||
] as const
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { GlobalSettings } from './global-settings-types'
|
||||
import type { ProjectExecutionRuntimeResolution } from './project-execution-runtime'
|
||||
import { isWindowsAbsolutePathLike } from './cross-platform-path'
|
||||
import {
|
||||
resolveLocalWindowsTerminalRuntimeOptions,
|
||||
type LocalWindowsTerminalRuntimeOptions
|
||||
} from './local-windows-terminal-runtime'
|
||||
import type { LocalWindowsTerminalRuntimeOptions } from './local-windows-terminal-runtime'
|
||||
import { resolveStartupShell, type AgentStartupShell } from './tui-agent-startup-shell'
|
||||
import { resolveLocalWindowsAgentStartupShell } from './windows-terminal-shell'
|
||||
import { isWslUncPath } from './wsl-paths'
|
||||
@@ -17,7 +13,6 @@ import { isWslUncPath } from './wsl-paths'
|
||||
export type AgentLaunchRuntime = {
|
||||
/** `linux` for a WSL runtime. */
|
||||
platform: NodeJS.Platform
|
||||
remote: boolean
|
||||
/** The spawn's own PTY options; empty for a remote host, which picks its own shell. */
|
||||
pty: LocalWindowsTerminalRuntimeOptions
|
||||
}
|
||||
@@ -27,7 +22,8 @@ export function agentLaunchQuotingShell(runtime: AgentLaunchRuntime): AgentStart
|
||||
runtime.platform,
|
||||
resolveLocalWindowsAgentStartupShell({
|
||||
platform: runtime.platform,
|
||||
isRemote: runtime.remote,
|
||||
// A remote fact names no shell, which quotes for its platform's default.
|
||||
isRemote: false,
|
||||
terminalWindowsShell: runtime.pty.shellOverride
|
||||
})
|
||||
)
|
||||
@@ -52,57 +48,3 @@ export function executionRuntimePlatform(
|
||||
export function remoteExecutionPlatform(workspacePath: string): NodeJS.Platform {
|
||||
return isWindowsAbsolutePathLike(workspacePath) ? 'win32' : 'linux'
|
||||
}
|
||||
|
||||
type LocalPtyInputs = {
|
||||
hostPlatform: NodeJS.Platform
|
||||
requestedShellOverride: string | undefined
|
||||
settings:
|
||||
| Partial<
|
||||
Pick<
|
||||
GlobalSettings,
|
||||
'terminalWindowsShell' | 'terminalWindowsWslDistro' | 'terminalDefaultShell'
|
||||
>
|
||||
>
|
||||
| undefined
|
||||
projectRuntime: ProjectExecutionRuntimeResolution | undefined
|
||||
/** The Windows shell that replaces a `wsl.exe` setting on a Windows-host runtime; absent is
|
||||
* `powershell.exe`, as the window's tabs pick. */
|
||||
fallbackHostShell?: string
|
||||
sshLoginShell: string | undefined
|
||||
}
|
||||
|
||||
/** A local PTY's shell and distro. Throws for a runtime that needs repair. */
|
||||
export function resolveLocalPtyRuntimeOptions(
|
||||
args: LocalPtyInputs
|
||||
): LocalWindowsTerminalRuntimeOptions {
|
||||
return args.hostPlatform === 'win32'
|
||||
? resolveLocalWindowsTerminalRuntimeOptions({
|
||||
requestedShellOverride: args.requestedShellOverride,
|
||||
settings: args.settings,
|
||||
projectRuntime: args.projectRuntime,
|
||||
fallbackHostShell: args.fallbackHostShell,
|
||||
sshLoginShell: args.sshLoginShell
|
||||
})
|
||||
: {
|
||||
shellOverride:
|
||||
args.requestedShellOverride ?? (args.settings?.terminalDefaultShell || undefined),
|
||||
terminalWindowsWslDistro: null
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveAgentLaunchRuntime(
|
||||
args: LocalPtyInputs & { remote: boolean; workspacePath: string }
|
||||
): AgentLaunchRuntime {
|
||||
if (args.remote) {
|
||||
return {
|
||||
platform: remoteExecutionPlatform(args.workspacePath),
|
||||
remote: true,
|
||||
pty: { shellOverride: undefined, terminalWindowsWslDistro: null }
|
||||
}
|
||||
}
|
||||
return {
|
||||
platform: executionRuntimePlatform(args.projectRuntime, args.workspacePath, args.hostPlatform),
|
||||
remote: false,
|
||||
pty: resolveLocalPtyRuntimeOptions(args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { Project } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import {
|
||||
findFolderWorkspaceCandidateRepos,
|
||||
resolveFolderWorkspaceHost,
|
||||
type FolderWorkspaceHostState
|
||||
} from './folder-workspace-execution-host'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host'
|
||||
import {
|
||||
deriveGlobalWindowsRuntimeDefaultFromLegacySettings,
|
||||
getWorkspaceRuntimePreference,
|
||||
resolveProjectExecutionRuntime,
|
||||
type LegacyWindowsRuntimeSettings,
|
||||
type ProjectExecutionRuntimeResolution
|
||||
} from './project-execution-runtime'
|
||||
|
||||
/** The one local repo whose project runs a folder workspace on this machine; none for a folder
|
||||
* pinned to another host or spanning several repos. */
|
||||
export function findLocalFolderWorkspaceRuntimeRepo(
|
||||
state: FolderWorkspaceHostState,
|
||||
folderWorkspaceId: string
|
||||
): Repo | undefined {
|
||||
const folder = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId)
|
||||
if (!folder || resolveFolderWorkspaceHost(state, folder.id).kind !== 'local') {
|
||||
return undefined
|
||||
}
|
||||
const group = state.projectGroups.find((entry) => entry.id === folder.projectGroupId)
|
||||
if (
|
||||
(folder.executionHostId ?? group?.executionHostId ?? LOCAL_EXECUTION_HOST_ID) !==
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const candidates = findFolderWorkspaceCandidateRepos(state, folder.id)
|
||||
const repo = candidates.length === 1 ? candidates[0] : undefined
|
||||
return repo && getRepoExecutionHostId(repo) === LOCAL_EXECUTION_HOST_ID ? repo : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A local repo workspace's runtime, read the same way by the window and the host: its project's
|
||||
* saved runtime, else the distro of a WSL-share path, else the global default.
|
||||
*/
|
||||
export function resolveLocalWorkspaceExecutionRuntime(args: {
|
||||
appPlatform: NodeJS.Platform
|
||||
projects:
|
||||
| readonly Pick<Project, 'id' | 'sourceRepoIds' | 'localWindowsRuntimePreference'>[]
|
||||
| undefined
|
||||
settings:
|
||||
| (LegacyWindowsRuntimeSettings & { localWindowsRuntimeDefault?: unknown })
|
||||
| null
|
||||
| undefined
|
||||
repoId: string
|
||||
/** The workspace's own project; absent looks the project up by repo. */
|
||||
projectId?: string
|
||||
workspacePath: string | null | undefined
|
||||
wslAvailable?: boolean
|
||||
availableWslDistros?: readonly string[] | null
|
||||
}): ProjectExecutionRuntimeResolution {
|
||||
const project = args.projects?.find(
|
||||
(entry) =>
|
||||
entry.id === args.projectId ||
|
||||
entry.id === args.repoId ||
|
||||
entry.sourceRepoIds?.includes(args.repoId)
|
||||
)
|
||||
return resolveProjectExecutionRuntime({
|
||||
appPlatform: args.appPlatform,
|
||||
projectId: args.projectId ?? project?.id ?? args.repoId,
|
||||
projectRuntimePreference: getWorkspaceRuntimePreference(
|
||||
project?.localWindowsRuntimePreference,
|
||||
args.workspacePath
|
||||
),
|
||||
globalWindowsRuntimeDefault:
|
||||
args.settings?.localWindowsRuntimeDefault ??
|
||||
deriveGlobalWindowsRuntimeDefaultFromLegacySettings(args.settings).defaultRuntime,
|
||||
wslAvailable: args.wslAvailable,
|
||||
availableWslDistros: args.availableWslDistros
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* The wire shape of `agent.launch`, mirroring `AgentLaunchIntent`.
|
||||
*
|
||||
* A caller states WHERE the agent lands and WHAT it should say; it does not pick a chat. Its one
|
||||
* mode input is `terminalOnly`, for a caller whose surface is a terminal tab, and there is no
|
||||
* startup-agent field on the create payload — the host decides, and
|
||||
* `withoutReservedAgentCreateFields` strips a stale one out of a payload a caller migrated over
|
||||
* from `worktree.create`.
|
||||
* A caller states WHERE the agent lands and WHAT it should say; it never names a mode. There is
|
||||
* deliberately no `structured` / `terminal` field and no startup-agent field on the create
|
||||
* payload — the host decides, and `withoutReservedAgentCreateFields` strips a stale one out of a
|
||||
* payload a caller migrated over from `worktree.create`.
|
||||
*
|
||||
* Shared rather than main-side because every field resolves to a shared schema: a remote client
|
||||
* that sends this method needs `RpcSendParams<'agent.launch'>` to exist, and a method missing from
|
||||
@@ -93,15 +92,6 @@ export const AgentLaunchFields = z.object({
|
||||
* structured launch that carries one rather than ignoring it. */
|
||||
cwd: z.string().min(1, 'Empty launch cwd').optional(),
|
||||
launchSource: LaunchSourceParam.optional(),
|
||||
// These four are read by hosts advertising `agent.launch.terminal-inputs.v1`; older hosts drop them.
|
||||
/** The terminal tab's title. Ignored by a structured launch and a reused terminal. */
|
||||
title: z.string().optional(),
|
||||
/** An automation's saved extras, merged over the resolved arguments; invalid extras refuse. */
|
||||
extraAgentArgs: z.string().optional(),
|
||||
/** `agent_started.request_kind`; open like `launchSource`, so an unknown word reads as `new`. */
|
||||
requestKind: z.string().optional(),
|
||||
/** Start a terminal agent whatever the chat default says: the caller's surface is a terminal. */
|
||||
terminalOnly: z.boolean().optional(),
|
||||
/**
|
||||
* The pane a terminal launch should create, minted by a caller that places its own tabs.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user