mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
Round-1 review of the rebuilt step 3. - One outcome, switched on by every caller. planLaunchPrompt answers none, on-line, launch-file or paste-after-ready; only the carried outcomes hold the plan to launch, so no caller can read "a plan exists" as "carried". buildAgentStartupPlan now builds only a prompt-less launch. The new agent tab and work items had dropped a prompt an agent could not take in a file (any agent but Claude and Codex) and reported it delivered; they now paste it. The composer paths, background sessions, agent.launch, agentSession.create and phone quick commands switch on the same outcome. - One rule for hosts that type the line raw. A Windows host or a paired Orca keeps a line to #24257's 512-byte typed budget; anything longer, multi-line or damaged by the shell rides a launch file for Claude and Codex and is pasted for others. A paired host is one predicate (launchHostIsPaired). - Delivered means the agent got it. A prompt the new tab's line carried resolves its delivery when the agent's own hook reports a turn or the agent shows its composer, and false when the PTY never spawns (a refused launch file) or exits first, so callers that resolve review threads or mark notes delivered wait for that, not for the tab. - Daemon protocol v41 writes launch files. A spawn routed to an older daemon is refused with the launch-file reason instead of typing a pointer to a file nobody wrote. - A refused launch file or WSL staged line reaches agent.launch as its reason instead of a 10 s handle timeout, and hands the prompt back with Copy prompt for a staged line too. - A WSL distro is probed only when the spawn needs its directory, and a failed probe is remembered for 30 s. - Every hook status row shows the prompt a launch file carries instead of its pointer sentence.
198 lines
7.1 KiB
TypeScript
198 lines
7.1 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,
|
|
planLaunchPrompt,
|
|
type AgentStartupPlan
|
|
} from '../../shared/tui-agent-startup'
|
|
import type { LaunchFile } from '../../shared/launch-prompt-file'
|
|
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 pastes an uncarried prompt itself; reports whether it rode the command,
|
|
* and no follow-up is returned for it to paste twice. */
|
|
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 planned = planLaunchPrompt({ ...planInputs, prompt: environment.prompt ?? '' })
|
|
if (!planned) {
|
|
throw new Error(`Could not build launch command for ${agent}.`)
|
|
}
|
|
let startupPlan: AgentStartupPlan
|
|
let launchFile: LaunchFile | undefined
|
|
let followup: WorktreeStartupFollowup | undefined
|
|
switch (planned.carry) {
|
|
case 'none':
|
|
startupPlan = planned.plan
|
|
break
|
|
case 'on-line':
|
|
case 'launch-file':
|
|
startupPlan = planned.plan
|
|
launchFile = planned.carry === 'launch-file' ? planned.launchFile : undefined
|
|
environment.onPromptCarry?.(true)
|
|
break
|
|
case 'paste-after-ready':
|
|
startupPlan = planned.cleanPlan
|
|
if (environment.onPromptCarry) {
|
|
environment.onPromptCarry(false)
|
|
} else {
|
|
followup = { expectedProcess: startupPlan.expectedProcess, prompt: planned.text }
|
|
}
|
|
break
|
|
}
|
|
return {
|
|
agent,
|
|
startup: {
|
|
command: startupPlan.launchCommand,
|
|
launchConfig: startupPlan.launchConfig,
|
|
...(startupPlan.startupCommandDelivery
|
|
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
|
|
: {}),
|
|
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
|
...(launchFile ? { launchFile } : {}),
|
|
telemetry: agentStartedTelemetry(agent, environment.launchSource)
|
|
},
|
|
...(followup ? { followup } : {})
|
|
}
|
|
}
|