Files
orca/src/main/runtime/runtime-worktree-agent-startup.ts
T
Brennan Benson d96b1e75f2 fix(agent-launch): paste a launch prompt only when the launched agent is proven in front
A launch pasted its prompt unless a shell was proven in the terminal's
foreground, so any read that could not prove one let the prompt through. After
an agent exited at startup, its shell turned bracketed paste on at the next
prompt, readiness fired on it, and the prompt was typed into the shell:

- macOS: a pane runs its shell under login, so the process-group fence's root
  was never the shell's group and never proved it; the cached foreground name
  could also still name the exited process.
- Windows Git Bash and WSL: the shell-alone-in-its-job check never answers.

Now one fresh read of the terminal's foreground decides: agent, shell or
unknown. Only 'agent' lets a write through (paste, Enter, second Enter, reused
panes too); 'shell' still drops a ready signal. A Windows host never proves the
agent, so there the launch line carries the prompt at any size, as on main.
2026-10-02 01:26:36 -07:00

202 lines
7.4 KiB
TypeScript

import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry'
import type { AgentLaunchPreferences } from '../../shared/agent-session-host-authority'
import type { Repo } from '../../shared/repo-types'
import type { TuiAgent } from '../../shared/tui-agent'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import { repoIsRemote } from '../../shared/agent-launch-remote'
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 { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '../../shared/tui-agent-startup'
import { planStartupWithPromptCandidate } from '../../shared/startup-line-prompt-carry'
import {
launchHostProvesAgentInFront,
nameLocalTypedLineShell
} from './agent-launch-typed-line-shell'
import {
detectInstalledAgentsWithShellPathHydration,
detectRemoteAgents
} from '../preflight/agent-detection'
import type { RuntimeStore } from './runtime-store-contract'
export type WorktreeStartupDraftPaste = { agent: TuiAgent; content: string }
export type WorktreeStartupFollowup = { expectedProcess: string; prompt: string }
/** A fresh agent the host builds always carries its `agent_started` record; dropping it fails to compile. */
type AttributedWorktreeStartupLaunch = WorktreeStartupLaunch & {
telemetry: NonNullable<WorktreeStartupLaunch['telemetry']>
}
type StartupEnvironment = {
repo: Repo
settings: ReturnType<RuntimeStore['getSettings']>
getLaunchPlatform: () => NodeJS.Platform
/** Replaces the configured arguments for this launch; `null` means none. */
agentArgs?: string | null
/** Caller-supplied telemetry attribution, validated leniently at the host boundary. */
launchSource?: string
}
export async function buildWorktreeStartupForDraft(
environment: StartupEnvironment & { draft: string; requestedAgent?: TuiAgent }
): Promise<{
agent: TuiAgent
startup: AttributedWorktreeStartupLaunch
draftPaste?: WorktreeStartupDraftPaste
} | null> {
const content = environment.draft.trim()
if (!content) {
return null
}
const { repo, settings } = environment
const preferredAgent = environment.requestedAgent ?? settings.defaultTuiAgent
// Why: `blank` is an explicit shell-only preference, so linked drafts must not auto-pick an agent.
if (preferredAgent === 'blank') {
return null
}
let agent =
isTuiAgent(preferredAgent) && isTuiAgentEnabled(preferredAgent, settings.disabledTuiAgents)
? preferredAgent
: null
if (!agent) {
let detected: string[] = []
// Why: detection has to run on the machine that will run the agent, and SSH ownership has two
// spellings — the raw field probes this client for an `executionHostId: 'ssh:*'`-only repo.
const sshConnectionId = getRepoSshConnectionId(repo)
try {
// Why: startup-draft fallback can run from sparse runtime launch envs too.
detected = sshConnectionId
? await detectRemoteAgents({ connectionId: sshConnectionId })
: await detectInstalledAgentsWithShellPathHydration()
} catch {
detected = []
}
agent = pickTuiAgent(null, detected.filter(isTuiAgent), settings.disabledTuiAgents)
}
if (!agent) {
return null
}
const launchArgs = resolveAgentStartupPlanInputs({
agent,
settings,
platform: environment.getLaunchPlatform(),
isRemote: repoIsRemote(repo),
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {})
})
const telemetry = agentStartedTelemetry(agent, environment.launchSource)
const draftPlan = buildAgentDraftLaunchPlan({ ...launchArgs, draft: content })
if (draftPlan) {
return {
agent,
startup: {
command: draftPlan.launchCommand,
launchConfig: draftPlan.launchConfig,
...(draftPlan.startupCommandDelivery
? { startupCommandDelivery: draftPlan.startupCommandDelivery }
: {}),
...(draftPlan.env ? { env: draftPlan.env } : {}),
telemetry
}
}
}
const startupPlan = buildAgentStartupPlan({
...launchArgs,
prompt: '',
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
return null
}
return {
agent,
startup: {
command: startupPlan.launchCommand,
launchConfig: startupPlan.launchConfig,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
telemetry
},
draftPaste: { agent, content }
}
}
export function buildWorktreeStartupForAgent(
environment: StartupEnvironment & {
agent: TuiAgent
prompt?: string
launchPreferences?: AgentLaunchPreferences
toSessionOptions: (
preferences?: AgentLaunchPreferences
) => Parameters<typeof buildAgentStartupPlan>[0]['sessionOptions'] | undefined
/** Set by a caller that delivers an uncarried prompt itself: the prompt then rides only a typed
* line that can carry it, and this reports whether it did. Absent keeps the CLI's fold. */
onPromptCarry?: (carried: boolean) => void
}
): {
agent: TuiAgent
startup: AttributedWorktreeStartupLaunch
followup?: WorktreeStartupFollowup
} {
const { agent, repo, settings } = environment
if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) {
throw new Error('Selected agent is disabled. Choose an enabled agent before creating.')
}
const planInputs = resolveAgentStartupPlanInputs({
agent,
settings,
platform: environment.getLaunchPlatform(),
isRemote: repoIsRemote(repo),
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {}),
sessionOptions: environment.toSessionOptions(environment.launchPreferences)
})
const prompt = environment.prompt ?? ''
let startupPlan: ReturnType<typeof buildAgentStartupPlan>
if (environment.onPromptCarry) {
const offered = planStartupWithPromptCandidate(planInputs, prompt, {
shellName: nameLocalTypedLineShell({
isRemote: repoIsRemote(repo),
...(settings.terminalDefaultShell
? { defaultShellSetting: settings.terminalDefaultShell }
: {})
}),
provesAgentInFront: launchHostProvesAgentInFront({
isRemote: repoIsRemote(repo),
launchPlatform: environment.getLaunchPlatform()
})
})
startupPlan = offered.plan
if (startupPlan && prompt.trim()) {
environment.onPromptCarry(offered.promptCarried)
}
} else {
startupPlan = buildAgentStartupPlan({ ...planInputs, prompt, allowEmptyPromptLaunch: true })
}
if (!startupPlan) {
throw new Error(`Could not build launch command for ${agent}.`)
}
return {
agent,
startup: {
command: startupPlan.launchCommand,
launchConfig: startupPlan.launchConfig,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
telemetry: agentStartedTelemetry(agent, environment.launchSource)
},
...(startupPlan.followupPrompt
? {
followup: {
expectedProcess: startupPlan.expectedProcess,
prompt: startupPlan.followupPrompt
}
}
: {})
}
}