From 1df8cf20e77edd01f982469b359d431f3cfff4f1 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:25:46 -0700 Subject: [PATCH] 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. --- .../agent-launch/agent-launch-execution.ts | 4 +- .../agent-launch/agent-launch-executor.ts | 18 ++- src/main/agent-launch/agent-launch-mode.ts | 14 +- .../agent-launch-surface-factories.ts | 26 +--- .../agent-launch/agent-started-telemetry.ts | 18 ++- .../spawn-preflight-requested-shell.test.ts | 1 - src/main/local-project-runtime-resolution.ts | 135 +++++++++++++----- .../preflight/workspace-agent-detection.ts | 2 +- .../agent-launch-scope-runtime.test.ts | 81 +++++------ .../runtime/agent-launch-scope-runtime.ts | 96 +++++++------ .../orca-runtime-activate-managed-worktree.ts | 10 +- .../orca-runtime-create-managed-worktree.ts | 1 + ...a-runtime-get-terminal-interactive-wait.ts | 29 ++-- ...form-mobile-session-pty-records-refresh.ts | 9 +- ...tore-structured-agent-session-tabs-once.ts | 35 ++--- ...time-run-create-mobile-session-terminal.ts | 1 + ...a-runtime-terminal-create-deduplication.ts | 11 +- .../methods/agent-launch-intent-resolution.ts | 6 +- .../rpc/methods/agent-launch-surfaces.ts | 8 +- .../methods/agent-launch-tab-publication.ts | 3 +- .../methods/agent-launch-worktree-creation.ts | 8 +- src/main/runtime/rpc/methods/agent-launch.ts | 5 +- .../runtime-agent-terminal-startup.test.ts | 34 +---- .../runtime/runtime-agent-terminal-startup.ts | 97 +++++-------- .../runtime/runtime-folder-worktree-create.ts | 1 - ...runtime-local-worktree-terminal-startup.ts | 1 - .../runtime-managed-worktree-create-types.ts | 4 - .../runtime/runtime-terminal-contracts.ts | 17 --- .../runtime-worktree-agent-startup.test.ts | 18 ++- .../runtime/runtime-worktree-agent-startup.ts | 39 +++-- .../terminal-shell-override-host-support.ts | 17 +-- src/renderer/src/lib/execution-host-facts.ts | 20 ++- .../src/lib/local-preflight-context.ts | 56 ++++++-- .../lib/local-project-runtime-workspace.ts | 22 ++- src/shared/agent-launch-intent.ts | 11 +- src/shared/agent-launch-operation.test.ts | 14 +- src/shared/agent-launch-operation.ts | 13 +- src/shared/agent-launch-runtime-capability.ts | 8 +- src/shared/agent-launch-runtime.ts | 64 +-------- .../local-workspace-execution-runtime.ts | 78 ---------- .../rpc-contract/agent-launch-params.ts | 18 +-- 41 files changed, 423 insertions(+), 630 deletions(-) delete mode 100644 src/shared/local-workspace-execution-runtime.ts diff --git a/src/main/agent-launch/agent-launch-execution.ts b/src/main/agent-launch/agent-launch-execution.ts index 38625fa109a..4568c3a70bd 100644 --- a/src/main/agent-launch/agent-launch-execution.ts +++ b/src/main/agent-launch/agent-launch-execution.ts @@ -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 diff --git a/src/main/agent-launch/agent-launch-executor.ts b/src/main/agent-launch/agent-launch-executor.ts index a481c906e08..45caee5d144 100644 --- a/src/main/agent-launch/agent-launch-executor.ts +++ b/src/main/agent-launch/agent-launch-executor.ts @@ -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. diff --git a/src/main/agent-launch/agent-launch-mode.ts b/src/main/agent-launch/agent-launch-mode.ts index 49f02ebfa14..874f5695656 100644 --- a/src/main/agent-launch/agent-launch-mode.ts +++ b/src/main/agent-launch/agent-launch-mode.ts @@ -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) { diff --git a/src/main/agent-launch/agent-launch-surface-factories.ts b/src/main/agent-launch/agent-launch-surface-factories.ts index 633bf306a18..9166bba4fa8 100644 --- a/src/main/agent-launch/agent-launch-surface-factories.ts +++ b/src/main/agent-launch/agent-launch-surface-factories.ts @@ -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> - /** 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> }): 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 } : {}) - } -} diff --git a/src/main/agent-launch/agent-started-telemetry.ts b/src/main/agent-launch/agent-started-telemetry.ts index 6274b5e20f6..2d1ff76179d 100644 --- a/src/main/agent-launch/agent-started-telemetry.ts +++ b/src/main/agent-launch/agent-started-telemetry.ts @@ -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 { 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' } } diff --git a/src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts b/src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts index 4e1c9581384..fba3c22ae9d 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts @@ -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' } } }) diff --git a/src/main/local-project-runtime-resolution.ts b/src/main/local-project-runtime-resolution.ts index fea481dc178..50d742ae1ec 100644 --- a/src/main/local-project-runtime-resolution.ts +++ b/src/main/local-project-runtime-resolution.ts @@ -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, settings: ReturnType = 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[0]['settings'], + settings: Parameters[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' + ) } diff --git a/src/main/preflight/workspace-agent-detection.ts b/src/main/preflight/workspace-agent-detection.ts index 9b5b512c0a0..8787b856a42 100644 --- a/src/main/preflight/workspace-agent-detection.ts +++ b/src/main/preflight/workspace-agent-detection.ts @@ -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' diff --git a/src/main/runtime/agent-launch-scope-runtime.test.ts b/src/main/runtime/agent-launch-scope-runtime.test.ts index e8e5b0e5e61..f3cc1433c53 100644 --- a/src/main/runtime/agent-launch-scope-runtime.test.ts +++ b/src/main/runtime/agent-launch-scope-runtime.test.ts @@ -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' + ]) + } +) diff --git a/src/main/runtime/agent-launch-scope-runtime.ts b/src/main/runtime/agent-launch-scope-runtime.ts index d14576c0fd2..5bd902d447b 100644 --- a/src/main/runtime/agent-launch-scope-runtime.ts +++ b/src/main/runtime/agent-launch-scope-runtime.ts @@ -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)) } } } diff --git a/src/main/runtime/orca-runtime-activate-managed-worktree.ts b/src/main/runtime/orca-runtime-activate-managed-worktree.ts index 55fb186f3ee..fe445d97917 100644 --- a/src/main/runtime/orca-runtime-activate-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-activate-managed-worktree.ts @@ -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 { 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) }) } diff --git a/src/main/runtime/orca-runtime-create-managed-worktree.ts b/src/main/runtime/orca-runtime-create-managed-worktree.ts index 8f2df2de440..84059bd28a2 100644 --- a/src/main/runtime/orca-runtime-create-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-create-managed-worktree.ts @@ -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 diff --git a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts index 0a28a208d41..c096875ab71 100644 --- a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts +++ b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts @@ -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, diff --git a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts index 5c43076b867..b481b4b5b29 100644 --- a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts +++ b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts @@ -174,14 +174,19 @@ export class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh extends OrcaRu ReturnType > = {} 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 { diff --git a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts index 2ee770a13c8..b6f34d0396a 100644 --- a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts +++ b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts @@ -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 { - 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 { + return resolveStartupAgentLaunchRuntime( + this.requireStore(), + scope ?? { + id: repo.id, + path: repo.path, + connectionId: getRepoSshConnectionId(repo), + repo, + folderWorkspace: null + } + ) } } diff --git a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts b/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts index c9de7d995f2..0813497d2de 100644 --- a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts +++ b/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts @@ -234,6 +234,7 @@ export class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWi targetGroupId: opts.targetGroupId, supportsSplitGroupPlacement: opts.supportsSplitGroupPlacement, launchConfig: startupCommand.launchConfig, + launchRuntime: startupCommand.launchRuntime, signal: opts.signal } ) diff --git a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts index 525bf2ad143..7d756c188d9 100644 --- a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts +++ b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts @@ -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 } diff --git a/src/main/runtime/rpc/methods/agent-launch-intent-resolution.ts b/src/main/runtime/rpc/methods/agent-launch-intent-resolution.ts index b76a7a3daac..be113c90c28 100644 --- a/src/main/runtime/rpc/methods/agent-launch-intent-resolution.ts +++ b/src/main/runtime/rpc/methods/agent-launch-intent-resolution.ts @@ -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 } : {}) } } diff --git a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts index 138bbaef952..0727f31bef0 100644 --- a/src/main/runtime/rpc/methods/agent-launch-surfaces.ts +++ b/src/main/runtime/rpc/methods/agent-launch-surfaces.ts @@ -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 diff --git a/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts b/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts index 772fb6d67fb..26770ed49d9 100644 --- a/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts +++ b/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts @@ -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') { diff --git a/src/main/runtime/rpc/methods/agent-launch-worktree-creation.ts b/src/main/runtime/rpc/methods/agent-launch-worktree-creation.ts index 54387eee63e..02f1bb86911 100644 --- a/src/main/runtime/rpc/methods/agent-launch-worktree-creation.ts +++ b/src/main/runtime/rpc/methods/agent-launch-worktree-creation.ts @@ -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 diff --git a/src/main/runtime/rpc/methods/agent-launch.ts b/src/main/runtime/rpc/methods/agent-launch.ts index d0bd6d8d404..5a37362e916 100644 --- a/src/main/runtime/rpc/methods/agent-launch.ts +++ b/src/main/runtime/rpc/methods/agent-launch.ts @@ -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 diff --git a/src/main/runtime/runtime-agent-terminal-startup.test.ts b/src/main/runtime/runtime-agent-terminal-startup.test.ts index bb0e8583691..b6cc5ea5f2e 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.test.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.test.ts @@ -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' } } ) diff --git a/src/main/runtime/runtime-agent-terminal-startup.ts b/src/main/runtime/runtime-agent-terminal-startup.ts index 88fa96666c2..61530a82ec9 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.ts @@ -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) }) } diff --git a/src/main/runtime/runtime-folder-worktree-create.ts b/src/main/runtime/runtime-folder-worktree-create.ts index 405bb43565d..9d42c80289e 100644 --- a/src/main/runtime/runtime-folder-worktree-create.ts +++ b/src/main/runtime/runtime-folder-worktree-create.ts @@ -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 } : {}), diff --git a/src/main/runtime/runtime-local-worktree-terminal-startup.ts b/src/main/runtime/runtime-local-worktree-terminal-startup.ts index 815019fbd3d..90f0863c857 100644 --- a/src/main/runtime/runtime-local-worktree-terminal-startup.ts +++ b/src/main/runtime/runtime-local-worktree-terminal-startup.ts @@ -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, diff --git a/src/main/runtime/runtime-managed-worktree-create-types.ts b/src/main/runtime/runtime-managed-worktree-create-types.ts index 8a568aba209..0a9d241d8c7 100644 --- a/src/main/runtime/runtime-managed-worktree-create-types.ts +++ b/src/main/runtime/runtime-managed-worktree-create-types.ts @@ -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 diff --git a/src/main/runtime/runtime-terminal-contracts.ts b/src/main/runtime/runtime-terminal-contracts.ts index 48162be3290..55d5a61b5f8 100644 --- a/src/main/runtime/runtime-terminal-contracts.ts +++ b/src/main/runtime/runtime-terminal-contracts.ts @@ -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 diff --git a/src/main/runtime/runtime-worktree-agent-startup.test.ts b/src/main/runtime/runtime-worktree-agent-startup.test.ts index db18adb0f36..2ae5e317214 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.test.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.test.ts @@ -33,11 +33,11 @@ function makeRepo(fields: Partial): 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', diff --git a/src/main/runtime/runtime-worktree-agent-startup.ts b/src/main/runtime/runtime-worktree-agent-startup.ts index 541ad7bd0ff..e47b8858558 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.ts @@ -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 - /** 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 } : {}) }) } diff --git a/src/main/runtime/terminal-shell-override-host-support.ts b/src/main/runtime/terminal-shell-override-host-support.ts index ff9b0495541..6a08508f7fa 100644 --- a/src/main/runtime/terminal-shell-override-host-support.ts +++ b/src/main/runtime/terminal-shell-override-host-support.ts @@ -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}.` diff --git a/src/renderer/src/lib/execution-host-facts.ts b/src/renderer/src/lib/execution-host-facts.ts index e240faa1fd6..d6635931161 100644 --- a/src/renderer/src/lib/execution-host-facts.ts +++ b/src/renderer/src/lib/execution-host-facts.ts @@ -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 diff --git a/src/renderer/src/lib/local-preflight-context.ts b/src/renderer/src/lib/local-preflight-context.ts index 113a892a2a3..2eb0d9133df 100644 --- a/src/renderer/src/lib/local-preflight-context.ts +++ b/src/renderer/src/lib/local-preflight-context.ts @@ -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 | 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 diff --git a/src/renderer/src/lib/local-project-runtime-workspace.ts b/src/renderer/src/lib/local-project-runtime-workspace.ts index 9c8290ee790..1eda3cb9f67 100644 --- a/src/renderer/src/lib/local-project-runtime-workspace.ts +++ b/src/renderer/src/lib/local-project-runtime-workspace.ts @@ -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, diff --git a/src/shared/agent-launch-intent.ts b/src/shared/agent-launch-intent.ts index 61ef355d03b..0fbf3699562 100644 --- a/src/shared/agent-launch-intent.ts +++ b/src/shared/agent-launch-intent.ts @@ -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 } /** diff --git a/src/shared/agent-launch-operation.test.ts b/src/shared/agent-launch-operation.test.ts index a89864cfd22..6b24b934fb3 100644 --- a/src/shared/agent-launch-operation.test.ts +++ b/src/shared/agent-launch-operation.test.ts @@ -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', () => { diff --git a/src/shared/agent-launch-operation.ts b/src/shared/agent-launch-operation.ts index fdd2b9b21f4..920841d568c 100644 --- a/src/shared/agent-launch-operation.ts +++ b/src/shared/agent-launch-operation.ts @@ -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 }) } diff --git a/src/shared/agent-launch-runtime-capability.ts b/src/shared/agent-launch-runtime-capability.ts index aa3799d352f..b751c13b9c4 100644 --- a/src/shared/agent-launch-runtime-capability.ts +++ b/src/shared/agent-launch-runtime-capability.ts @@ -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 diff --git a/src/shared/agent-launch-runtime.ts b/src/shared/agent-launch-runtime.ts index 1a8aeab122f..c780448872e 100644 --- a/src/shared/agent-launch-runtime.ts +++ b/src/shared/agent-launch-runtime.ts @@ -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) - } -} diff --git a/src/shared/local-workspace-execution-runtime.ts b/src/shared/local-workspace-execution-runtime.ts deleted file mode 100644 index 77edabe22b7..00000000000 --- a/src/shared/local-workspace-execution-runtime.ts +++ /dev/null @@ -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[] - | 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 - }) -} diff --git a/src/shared/rpc-contract/agent-launch-params.ts b/src/shared/rpc-contract/agent-launch-params.ts index 56cd220fc38..641f2eb4eb1 100644 --- a/src/shared/rpc-contract/agent-launch-params.ts +++ b/src/shared/rpc-contract/agent-launch-params.ts @@ -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. *