mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
A launch prompt for an agent whose CLI takes one could still be pasted into its screen once it looked ready: the host pasted any prompt whose typed line was over 512 bytes or held a newline (outside short-lined zsh), and the desktop's generated-prompt launches started the agent empty and pasted. A paste can lose text, land its Enter on a startup dialog (Codex's update prompt), or wait seconds for the agent to look ready. Where a launch prompt rides is now decided in one place, the startup-plan builder (carryLaunchPrompt), for every launch path: - on the agent's command line, which the previous step stages when the line is too long or multi-line to type; - in a private launch file the line points at, when the prompt is past 16,384 characters, sensitive, or one a Windows shell would damage (a control byte; on PowerShell a ", a %NAME% pair or a trailing \; past cmd's 8,191-character cap). The execution host writes it before typing the line, quoted for its shell, and grants Claude a read of its directory. A failed write refuses the spawn with a Copy-prompt notice; - pasted once the agent is ready only for an agent that takes text after start, a prompt needing a launch file its agent is not measured reading (only Claude and Codex are), a reused terminal, and a paired host, which is sent a command and never a file. This replaces the host's typed-line check (512 B, or zsh with every line <=512 B and <=8 KiB), which existed because nothing staged a long line. WSL writes its staged line and launch file into the distro and runs the line in the account's own login shell (sourced in bash, zsh and sh, eval'd in fish, through /bin/sh in ksh), with ~/.cache/orca made 0700 first. The "prompt wasn't sent" notice with Copy prompt lives here, shared by the new agent tab, background draft, paste timeout, work item and new workspace. Rebuilt on the restacked staging step; the earlier version sat on a base that carried #23748.
184 lines
6.7 KiB
TypeScript
184 lines
6.7 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 { planStartupWithLaunchPrompt } from '../../shared/startup-line-prompt-carry'
|
|
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 { plan: startupPlan, launchFile } = planStartupWithLaunchPrompt(
|
|
planInputs,
|
|
environment.prompt ?? ''
|
|
)
|
|
if (!startupPlan) {
|
|
throw new Error(`Could not build launch command for ${agent}.`)
|
|
}
|
|
if (environment.onPromptCarry && environment.prompt?.trim()) {
|
|
environment.onPromptCarry(startupPlan.followupPrompt === null)
|
|
}
|
|
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)
|
|
},
|
|
...(startupPlan.followupPrompt && !environment.onPromptCarry
|
|
? {
|
|
followup: {
|
|
expectedProcess: startupPlan.expectedProcess,
|
|
prompt: startupPlan.followupPrompt
|
|
}
|
|
}
|
|
: {})
|
|
}
|
|
}
|