Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
d268089
feat(agent-launch): host-side prompt delivery for agent.launch
brennanb2025 Oct 1, 2026
e463aea
fix(agent-launch): keep a short-lined multi-line prompt on a local zs…
brennanb2025 Oct 1, 2026
db66e25
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 1, 2026
9009914
test(agent-launch): keep the real-zsh launch-line test out of the Win…
brennanb2025 Oct 1, 2026
c798d08
refactor(protocol): move the agent.launch capabilities into their own…
brennanb2025 Oct 1, 2026
900a8ec
refactor(protocol): import the agent.launch capabilities from their o…
brennanb2025 Oct 1, 2026
8dc5516
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 1, 2026
aa5db2d
refactor(agent-launch): drop the unshipped viewMode field and trusted…
brennanb2025 Oct 2, 2026
2847488
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 2, 2026
f3a685e
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 2, 2026
71f9a21
fix(agent-launch): read Codex's provisional startup from the rule fil…
brennanb2025 Oct 2, 2026
39419d2
fix(agent-launch): hold rule-file name titles to quiet for a launch, …
brennanb2025 Oct 2, 2026
55fd9fe
fix(launch): stage long or multi-line launch lines so they arrive whole
brennanb2025 Oct 2, 2026
865328e
fix(launch): run a staged launch line as its own job in fish and ksh
brennanb2025 Oct 2, 2026
de94a21
fix(agent-launch): carry an agent's first prompt on its own command line
brennanb2025 Oct 2, 2026
d96b1e7
fix(agent-launch): paste a launch prompt only when the launched agent…
brennanb2025 Oct 2, 2026
2603d2c
test(agent-launch): cover the Windows QA stub, a grok override that e…
brennanb2025 Oct 2, 2026
74fa3f8
Merge #24257 head 2603d2cda18 (crash guard: paste only when the launc…
brennanb2025 Oct 2, 2026
d3fdcac
fix(agent-launch): make where a launch prompt rides explicit, and del…
brennanb2025 Oct 2, 2026
7c221ad
Merge #23962 head 74fa3f8e52f (#24257 crash guard) into the argv-carr…
brennanb2025 Oct 2, 2026
4635a64
docs(daemon): mark the pre-v41 launch-file refusal temporary, with wh…
brennanb2025 Oct 2, 2026
bc7ed25
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 2, 2026
1885364
Merge #24257 head bc7ed259a48 (origin/main 66799f7e8f0)
brennanb2025 Oct 2, 2026
9e5108a
fix(agent-launch): keep the local socket alive while a prompted launc…
brennanb2025 Oct 2, 2026
c9b4dc0
refactor(agent-launch): narrow the launch params by 'in' instead of a…
brennanb2025 Oct 2, 2026
57e7710
Merge #24257 head c9b4dc0578c (prompted agent.launch is a long poll)
brennanb2025 Oct 2, 2026
ea0c65d
fix(launch-prompt): one host-derived carry rule, receipts proven like…
brennanb2025 Oct 2, 2026
a29e42f
Merge #23962 head 57e77107b35 (#24257 long-poll launch, origin/main 6…
brennanb2025 Oct 2, 2026
7c0269b
test(launch-prompt): build the phone line-carry runtime from the defa…
brennanb2025 Oct 2, 2026
ea4699e
docs(launch-file): the sweep, not an age gate, is removeLaunchFile's …
brennanb2025 Oct 2, 2026
0599ae2
fix(launch-prompt): on Windows, paste what the line can't carry where…
brennanb2025 Oct 2, 2026
3054018
fix(agent-launch): find a launched agent behind a wrapper that leads …
brennanb2025 Oct 2, 2026
ef62954
Merge #24257 head 3054018722b (agent proven in front from the foregro…
brennanb2025 Oct 2, 2026
f8288f8
Merge #23962 head ef629543bd9 (#24257 3054018722b: agent proven in fr…
brennanb2025 Oct 2, 2026
6fe2527
fix(launch-prompt): one carry order on every platform; the line holds…
brennanb2025 Oct 2, 2026
b865c7d
fix(launch): type a plain Orca agent line as is in tcsh and nu
brennanb2025 Oct 2, 2026
d74371f
Merge #23962 head b865c7d07e9 (plain Orca agent line typed as is in t…
brennanb2025 Oct 2, 2026
29e38f6
fix(agent-launch): judge the foreground-group proof by when its captu…
brennanb2025 Oct 2, 2026
22c4f3e
Merge #24257 head 29e38f61cbd (group proof counts any capture begun a…
brennanb2025 Oct 2, 2026
f4bebe0
Merge #23962 head 22c4f3eebaf (#24257 29e38f61cbd: group proof judged…
brennanb2025 Oct 2, 2026
9be1739
fix(launch-prompt): "takes a launch file" is a host fact; such hosts …
brennanb2025 Oct 2, 2026
5c5f3f6
test(staging): read the stub agent's argv only once it is whole
brennanb2025 Oct 2, 2026
6bdfb96
refactor(launch-file): drop the unused sensitive flag
brennanb2025 Oct 2, 2026
c729128
fix(launch-prompt): judge a Windows line per shell from measured damage
brennanb2025 Oct 2, 2026
f02309f
test(agent-launch): keep the crash-guard live test out of the Windows…
brennanb2025 Oct 2, 2026
2126cee
Merge #24257 head f02309f8c6e (crash-guard live test: platform-neutra…
brennanb2025 Oct 2, 2026
636e306
Merge #23962 head 2126ceeae61 (#24257 f02309f8c6e, test-only) into th…
brennanb2025 Oct 2, 2026
e92f38d
fix(launch-prompt): the PowerShell a pane is spawned as is a host fact
brennanb2025 Oct 2, 2026
c4b2cbf
fix(launch-prompt): title a launch file's session by its prompt; say …
brennanb2025 Oct 2, 2026
4523a4b
perf(agent-launch): read a local pane's foreground from its own termi…
brennanb2025 Oct 2, 2026
e2a732e
Merge #24257 head 4523a4b3c4e (cheap pane-scoped foreground read)
brennanb2025 Oct 2, 2026
04417b6
Merge #23962 head e2a732e5469 (#24257 4523a4b3c4e: pane-scoped foregr…
brennanb2025 Oct 2, 2026
d8cfcf3
fix(launch-prompt): plan a WSL pane for its own shell; refuse a launc…
brennanb2025 Oct 2, 2026
ca1c404
fix(launch-prompt): plan a host that cannot write its staging folder …
brennanb2025 Oct 2, 2026
d260805
fix(launch-prompt): type main's line when the staging folder is unusa…
brennanb2025 Oct 2, 2026
b622714
fix(launch-prompt): refuse an unstageable line for callers whose prom…
brennanb2025 Oct 2, 2026
ae59440
fix(launch-prompt): report an exited agent only on proof it ran
brennanb2025 Oct 2, 2026
f29abc5
fix(launch-prompt): final review: paste where nothing can confirm, on…
brennanb2025 Oct 2, 2026
0a43c0a
test(launch-prompt): keep the cmd launch-file path case to what the p…
brennanb2025 Oct 2, 2026
0811b34
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 5, 2026
6c16914
test(mobile): re-measure the web app's script sweep after agent.launc…
brennanb2025 Oct 5, 2026
440978a
Merge remote-tracking branch 'origin/brennanb2025/agent-launch-host-p…
brennanb2025 Oct 5, 2026
962a0af
Merge #23962's new head into brennanb2025/launch-prompt-argv-carry
brennanb2025 Oct 5, 2026
7d4aede
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 5, 2026
cf4cb14
Merge #24257's new head (main 40f9e7a3fcd) into brennanb2025/launch-p…
brennanb2025 Oct 5, 2026
e2a98a6
Merge #23962's new head (main 40f9e7a3fcd) into brennanb2025/launch-p…
brennanb2025 Oct 5, 2026
f9bbc16
fix(agent-launch): a worker's brief needs its agent found in front, a…
brennanb2025 Oct 5, 2026
da05942
Merge #24257's new head (worker brief check, Grok worker readiness) i…
brennanb2025 Oct 5, 2026
9c51622
Merge #23962's new head (worker brief check, Grok worker readiness) i…
brennanb2025 Oct 5, 2026
ff0e1df
Merge remote-tracking branch 'origin/main' into brennanb2025/agent-la…
brennanb2025 Oct 5, 2026
ad337ab
Merge #24257's new head (main e2da3a1eb19) into brennanb2025/launch-p…
brennanb2025 Oct 5, 2026
1542cc8
Merge #23962's new head (main e2da3a1eb19) into brennanb2025/launch-p…
brennanb2025 Oct 5, 2026
83de487
Merge remote-tracking branch 'origin/main' into brennanb2025/launch-p…
brennanb2025 Oct 5, 2026
f5ed44e
Merge #23962's new head (main with #24257 squashed) into brennanb2025…
brennanb2025 Oct 5, 2026
620438e
Merge origin/main (#23962 squashed as 7f2541373a7) into brennanb2025/…
brennanb2025 Oct 6, 2026
04559df
Merge remote-tracking branch 'origin/main' into HEAD
brennanb2025 Oct 6, 2026
ce8fb5b
Merge origin/main 4e64fa9940f into brennanb2025/launch-prompt-argv-carry
brennanb2025 Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,7 @@ jobs:
src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts
src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts
src/shared/child-process/windows-command-line.win32.test.ts
src/shared/cmd-startup-arg-quoting.win32.test.ts
src/shared/child-process/windows-cmd-shim-resolution.test.ts
src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts
src/main/agent-hooks/windows-hook-payload-delivery.test.ts
Expand Down
1 change: 1 addition & 0 deletions config/scripts/pr-code-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ const WINDOWS_PACKAGE_TESTS = [
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
'src/shared/child-process/windows-command-line.win32.test.ts',
'src/shared/cmd-startup-arg-quoting.win32.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts',
'src/main/agent-hooks/windows-hook-payload-delivery.test.ts',
Expand Down
28 changes: 28 additions & 0 deletions src/main/agent-hooks/first-work-branch-rename.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildLaunchFilePointer } from '../../shared/launch-prompt-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { Repo } from '../../shared/repo-types'
Expand Down Expand Up @@ -258,6 +259,33 @@ describe('maybeAutoRenameBranchOnFirstWork', () => {
expect(onRenamed).toHaveBeenCalledWith(REPO_ID)
})

it('never names the branch after a launch-file pointer, and still renames on a later prompt', async () => {
const { deps, onRenamed } = makeDeps()
await maybeAutoRenameBranchOnFirstWork(
workingEvent({ prompt: buildLaunchFilePointer('/tmp/orca-launch-file-a1/task-context.md') }),
deps
)
expect(generateBranchNameMock).not.toHaveBeenCalled()
expect(onRenamed).not.toHaveBeenCalled()

await maybeAutoRenameBranchOnFirstWork(workingEvent({ prompt: 'Fix auth from note #1' }), deps)
expect(generateBranchNameMock).toHaveBeenCalledTimes(1)
})

it('names the branch from the prompt a launch file carried, when main kept it', async () => {
const { deps, onRenamed } = makeDeps({ showLaunchPrompt: () => 'Fix auth from note #1' })
await maybeAutoRenameBranchOnFirstWork(
workingEvent({ prompt: buildLaunchFilePointer('/tmp/orca-launch-file-a1/task-context.md') }),
deps
)
expect(generateBranchNameMock).toHaveBeenCalledWith(
expect.objectContaining({ firstPrompt: 'Fix auth from note #1' }),
expect.anything(),
expect.anything()
)
expect(onRenamed).toHaveBeenCalled()
})

it('asks to align the on-disk folder with the generated slug after renaming', async () => {
const { deps, renameWorktreeFolder } = makeDeps()
await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps)
Expand Down
12 changes: 11 additions & 1 deletion src/main/agent-hooks/first-work-branch-rename.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// On first agent work in a fresh workspace, replace the auto-generated creature branch (e.g. `you/Nautilus`) with a short work-derived name.
import { isLaunchFilePointer } from '../../shared/launch-prompt-file'
import type { GlobalSettings } from '../../shared/global-settings-types'
import type { Repo } from '../../shared/repo-types'
import { isFolderRepo } from '../../shared/repo-kind'
Expand Down Expand Up @@ -67,6 +68,8 @@ export type FirstWorkBranchRenameDeps = {
resolveWorktreeIdForTab: (tabId: string) => string | undefined
/** Invalidate caches + notify the renderer so the new branch name surfaces. */
onRenamed: (repoId: string) => void
/** The prompt Orca shows for a pane's reported one (`launchPromptShownForPane`). */
showLaunchPrompt?: (paneKey: string, prompt: string) => string
}

// inFlight blocks concurrent generation; settled caches definitive verdicts (transient bails stay unsettled to retry later).
Expand Down Expand Up @@ -114,7 +117,14 @@ export async function maybeAutoRenameBranchOnFirstWork(
if (settledWorktreeIds.has(worktreeId) || inFlightWorktreeIds.has(worktreeId)) {
return
}
const prompt = event.prompt?.trim()
const hookPrompt = event.prompt?.trim()
// Why the pointer: a launch that carried its prompt in a file names only that file here, which
// would name the branch after "read the task file". Its real prompt, when main kept it, names it
// instead; otherwise (no longer kept, as after a restart) a later prompt of the user's own can.
const shown = hookPrompt
? (deps.showLaunchPrompt?.(event.paneKey, hookPrompt) ?? hookPrompt).trim()
: undefined
const prompt = shown && !isLaunchFilePointer(shown) ? shown : undefined
if (!prompt) {
return
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/agent-hooks/first-work-rename-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { launchPromptShownForPane } from './launch-file-prompt-by-pane'
import { existsSync } from 'node:fs'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
Expand All @@ -21,6 +22,7 @@ export function firstWorkRenameDeps(
>
): FirstWorkBranchRenameDeps {
return {
showLaunchPrompt: launchPromptShownForPane,
getSettings: () => store.getSettings(),
getRepo: (repoId) => store.getRepo(repoId),
getAgentEnvResolvers: () => runtime.getCommitMessageAgentEnvironmentResolvers(),
Expand Down
34 changes: 34 additions & 0 deletions src/main/agent-hooks/launch-file-prompt-by-pane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { opensLikeLaunchFilePointer, type LaunchFile } from '../../shared/launch-prompt-file'

// Why bounded: an entry outlives its launch until newer ones push it out; the rename reads only a
// prompt's opening, and a pane's rename runs within minutes of its launch.
const KEPT_PANES = 50
const KEPT_CHARS = 4_000
const promptByPane = new Map<string, string>()

/**
* Keeps a launch file's prompt for the pane it launched: the agent's hook reports only the pointer
* to the file, and Orca shows the prompt instead (`launchPromptShownForPane`).
*/
export function rememberLaunchFilePrompt(
paneKey: string,
launchFile: LaunchFile | undefined
): void {
if (!launchFile) {
return
}
promptByPane.delete(paneKey)
promptByPane.set(paneKey, launchFile.content.slice(0, KEPT_CHARS))
for (const oldest of promptByPane.keys()) {
if (promptByPane.size <= KEPT_PANES) {
break
}
promptByPane.delete(oldest)
}
}

/** The one place Orca reads a pane's reported prompt or session title: a launch file's pointer,
* whole or cut short, reads as the prompt it points at, when this host kept it. */
export function launchPromptShownForPane(paneKey: string, prompt: string): string {
return opensLikeLaunchFilePointer(prompt) ? (promptByPane.get(paneKey) ?? prompt) : prompt
}
26 changes: 26 additions & 0 deletions src/main/agent-hooks/server-launch-file-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { makePaneKey } from '../../shared/stable-pane-id'
import { buildLaunchFilePointer, carryInLaunchFile } from '../../shared/launch-prompt-file'
import { AgentHookServer } from './server'
import { rememberLaunchFilePrompt } from './launch-file-prompt-by-pane'

const LAUNCHED = makePaneKey('tab-launched', '55555555-5555-4555-8555-555555555555')
const POINTER = buildLaunchFilePointer('/tmp/orca-launch-file-a1/task-context.md')

describe('a status row whose agent reports its launch file’s pointer', () => {
// Why: the agent's own hook sees only the pointer sentence; every Orca reader of the row (status
// row, dashboard, phone) must see the prompt the user wrote.
it('shows the prompt the file carries', () => {
rememberLaunchFilePrompt(LAUNCHED, carryInLaunchFile('fix the flaky test').launchFile)
const server = new AgentHookServer()
server.ingestTerminalStatus({
paneKey: LAUNCHED,
tabId: 'tab-launched',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: POINTER }
})
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({ paneKey: LAUNCHED, prompt: 'fix the flaky test' })
])
})
})
7 changes: 7 additions & 0 deletions src/main/agent-hooks/server/server-status-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { isStaleGrokTurnEnd } from './server-grok-status-rules'
import { resolveCancelVerdictLatch } from './server-cancel-verdict-latch'
import { AgentHookServerStatusApplication } from './server-status-application'
import { launchPromptShownForPane } from '../launch-file-prompt-by-pane'

export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusApplication {
protected applyNormalizedStatus(
Expand All @@ -28,6 +29,12 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
observedAt?: number,
mutationBefore?: EnrichedAgentHookEventPayload
): EnrichedAgentHookEventPayload | undefined {
// Why here: every status row (local, relayed, replayed) passes through, so no reader of the
// prompt (status row, dashboard, mobile) shows a launch file's pointer instead of the prompt.
const prompt = launchPromptShownForPane(incoming.paneKey, incoming.payload.prompt)
if (prompt !== incoming.payload.prompt) {
incoming = { ...incoming, payload: { ...incoming.payload, prompt } }
}
const transitioned = transitionHookPresence(
incoming,
this.state.lastStatusByPaneKey.get(incoming.paneKey)
Expand Down
24 changes: 21 additions & 3 deletions src/main/agent-launch/agent-launch-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function harness(options: {
structuredCreateError?: Error
deliveredMessageId?: string | null
terminalPromptDelivered?: boolean
/** Whether the surface reports that its typed line took the offered prompt. */
/** Whether the surface reports the offered prompt rode the launch command. */
lineCarriesPrompt?: boolean
}) {
const calls: string[] = []
Expand Down Expand Up @@ -355,7 +355,17 @@ describe('delivering a launch prompt to a terminal agent', () => {
expect(h.deliverTerminalPrompt).not.toHaveBeenCalled()
})

it('pastes an argv agent’s prompt after start when the surface reports its typed line could not carry it', async () => {
it('never pastes an argv agent’s prompt, however long: its launch command carries it', async () => {
const h = harness({ createSupport: { supported: false, reason: 'wsl' } })
const long = { delivery: 'submit' as const, text: `line one\n${'x'.repeat(20_000)}` }
const result = await h.run({ ...CREATE_INTENT, prompt: long })

expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'handed-to-terminal' })
expect(h.createTerminalAgent.mock.calls[0]?.[0]).toMatchObject({ startupPrompt: long.text })
expect(h.deliverTerminalPrompt).not.toHaveBeenCalled()
})

it('pastes after start when the surface reports the prompt needed a file the agent cannot read', async () => {
const h = harness({
createSupport: { supported: false, reason: 'wsl' },
lineCarriesPrompt: false
Expand All @@ -375,7 +385,7 @@ describe('delivering a launch prompt to a terminal agent', () => {
})
})

it('pastes into an agent-first create’s startup terminal when its typed line could not carry the prompt', async () => {
it('pastes into an agent-first create’s startup terminal that did not take the prompt', async () => {
const h = harness({ settings: null, lineCarriesPrompt: false })
const result = await h.run({ ...CREATE_INTENT, prompt: SUBMIT })

Expand All @@ -388,6 +398,14 @@ describe('delivering a launch prompt to a terminal agent', () => {
})
})

it('never pastes into an agent-first create’s startup terminal that took the prompt', async () => {
const h = harness({ settings: null })
const result = await h.run({ ...CREATE_INTENT, prompt: SUBMIT })

expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'handed-to-terminal' })
expect(h.deliverTerminalPrompt).not.toHaveBeenCalled()
})

it('writes into a reused terminal, whose process started before the launch existed', async () => {
const h = harness({})
const result = await h.run({
Expand Down
4 changes: 2 additions & 2 deletions src/main/agent-launch/agent-launch-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ async function resolveWorkspace(
startupTerminalHandle: string | undefined
startupTerminalPaneKey?: string
warning?: string
/** True when this create folded the prompt into the agent's startup command. */
/** True when this create put the prompt on the agent's startup command. */
promptRodeLaunchCommand?: boolean
}> {
const { intent } = execution
Expand Down Expand Up @@ -257,7 +257,7 @@ export type CreatedSurface = {
outcome: AgentLaunchResult['outcome']
warning?: string
structured?: AgentLaunchStructuredSurface
/** True when this create folded the prompt into the agent's launch command. */
/** True when this create put the prompt on the agent's launch command. */
promptRodeLaunchCommand?: boolean
}

Expand Down
26 changes: 13 additions & 13 deletions src/main/agent-launch/agent-launch-prompt-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
* surface exists and in what order; everything here decides how the text reaches whichever surface
* that turned out to be, and each surface takes it differently:
*
* structured session -> committed to the transcript, named by a message id -> journaled
* terminal, line fits -> folded into the command that execs the agent -> handed-to-terminal
* terminal, otherwise -> bracketed paste into the live PTY once it is ready -> handed-to-terminal
* anything unproven -> -> not-delivered
* structured session -> committed to the transcript, named by a message id -> journaled
* terminal, argv CLI -> on the command that execs the agent -> handed-to-terminal
* terminal, otherwise -> bracketed paste into the live PTY once it is ready -> handed-to-terminal
* anything unproven -> -> not-delivered
*
* argv has no readiness race, so it is offered wherever the agent's CLI takes a prompt argument
* (`agentPromptRidesLaunchCommand`). But that command is TYPED into the user's shell, and a long or
* multi-line typed line fails in ways argv itself does not, so whether the offer was taken is decided
* where the line is built (`startup-line-prompt-carry`) and reported back, never predicted here.
* argv has no readiness race, so it carries the prompt wherever the agent's CLI takes a prompt
* argument (`agentPromptRidesLaunchCommand`). Where it rides is decided once, where the line is built
* (`carryLaunchPrompt`): on the line, which the host stages when it is too long or multi-line to
* type, or in a launch file the line points at. A prompt the rule leaves for the paste (an agent
* not known to read a launch file, or a host that cannot write its staging folder) is pasted, and
* the surface reports that back.
*/

import type {
Expand All @@ -38,7 +40,7 @@ export async function settleLaunchPromptDisposal(
const messageId = await deliverStructuredLaunchPrompt(execution, created.structured)
return messageId ? { outcome: 'journaled', messageId } : NOT_DELIVERED
}
// The surface reported that the startup command carried the prompt; there is nothing left to write.
// The startup command carried the prompt; there is nothing left to write.
if (created.promptRodeLaunchCommand) {
return HANDED_TO_TERMINAL
}
Expand Down Expand Up @@ -102,10 +104,8 @@ function launchSubmitText(intent: AgentLaunchIntent): string | undefined {
return intent.prompt?.delivery === 'submit' && intent.prompt.text ? intent.prompt.text : undefined
}

/**
* The prompt offered to a terminal's launch command, which is an argv-mode agent's and only an
* argv-mode agent's. An offer, not a decision: the surface reports whether the typed line took it.
*/
/** The prompt a terminal's launch command carries, which is an argv-mode agent's and only an
* argv-mode agent's. */
export function argvLaunchPrompt(intent: AgentLaunchIntent): string | undefined {
const text = launchSubmitText(intent)
return text && agentPromptRidesLaunchCommand(intent.agent) ? text : undefined
Expand Down
13 changes: 7 additions & 6 deletions src/main/agent-launch/agent-launch-surface-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export type AgentLaunchSurfaceFactory = {
agent: TuiAgent
options?: Readonly<Record<string, unknown>>
/** Offered only for an agent whose CLI takes the prompt on argv. It rides the launch command
* only when the typed line can carry it; `promptRodeLaunchCommand` reports which happened. */
* unless `carryLaunchPrompt` leaves it for the paste; `promptRodeLaunchCommand` reports which
* happened. */
startupPrompt?: string
/** Replaces the settings default for this launch only; `null` means no arguments at all. */
agentArgs?: string | null
Expand All @@ -40,7 +41,7 @@ export type AgentLaunchSurfaceFactory = {
/** The pane this create minted; a factory whose runtime reports none omits it, never invents. */
paneKey?: string
warning?: string
/** Reported by the surface that built the typed line, never predicted by the executor. */
/** Reported by the surface that built the launch command, never predicted by the executor. */
promptRodeLaunchCommand?: boolean
}>
/**
Expand All @@ -59,8 +60,8 @@ export type AgentLaunchSurfaceFactory = {
* Writes the launch text into a terminal agent's live PTY, answering whether it landed.
*
* The other half of `startupPrompt`, for the cases the launch command cannot serve: a
* `stdin-after-start` agent, whose CLI takes no prompt argument; a prompt the typed line cannot
* carry; and a reused terminal, whose process was already running before this launch existed. `false` for every failure, on the same rule the structured
* `stdin-after-start` agent, whose CLI takes no prompt argument; a prompt `carryLaunchPrompt`
* leaves for the paste; and a reused terminal, whose process was already running before this launch existed. `false` for every failure, on the same rule the structured
* twin follows — a launch whose agent is running must not fail because its text did not land.
*/
deliverTerminalPrompt?(args: {
Expand Down Expand Up @@ -105,7 +106,7 @@ export type AgentLaunchWorkspaceFactory = {
* must await that gate explicitly instead. */
startupAgent: TuiAgent | undefined
/** Offered only alongside a `startupAgent` whose CLI takes the prompt on argv: agent-first
* creation builds the startup command, so that is where the typed line is measured. */
* creation builds the startup command, so that is where the carry is decided. */
startupPrompt?: string
/** Inputs needed when this terminal is created as the worktree's startup surface. */
agentArgs?: string | null
Expand All @@ -121,7 +122,7 @@ export type AgentLaunchWorkspaceFactory = {
startupTerminalPaneKey?: string
/** Created, but incomplete — surfaced on the launch result rather than dropped. */
warning?: string
/** Reported by the create that built the startup command's typed line. */
/** Reported by the create that built the startup command. */
promptRodeLaunchCommand?: boolean
}>
}
39 changes: 39 additions & 0 deletions src/main/ai-vault/session-title-launch-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
AiVaultSessionTitleRequest,
AiVaultSessionTitlesResult
} from '../../shared/ai-vault-session-title'
import { launchPromptShownForPane } from '../agent-hooks/launch-file-prompt-by-pane'
import { normalizeTitleText } from './session-scanner-text-normalization'

/**
* Titles a launch file's session with the prompt it carried. Its transcript opens with Orca's
* pointer, so the tab label and agent row would otherwise show "The full task is in the file …".
*/
export function showLaunchPromptsInSessionTitles(
requests: readonly AiVaultSessionTitleRequest[],
result: AiVaultSessionTitlesResult
): AiVaultSessionTitlesResult {
const paneBySession = new Map(
requests.flatMap((request) =>
request.paneKey ? [[`${request.agent}\0${request.sessionId}`, request.paneKey] as const] : []
)
)
if (paneBySession.size === 0) {
return result
}
return {
titles: result.titles.map((title) => {
const paneKey = paneBySession.get(`${title.agent}\0${title.sessionId}`)
const shown = paneKey ? launchPromptShownForPane(paneKey, title.title) : title.title
return shown === title.title
? title
: { ...title, title: normalizeTitleText(shown) ?? title.title }
})
}
}

/** A request as another host receives it: the pane is this host's to read. */
export function withoutPaneKey(request: AiVaultSessionTitleRequest): AiVaultSessionTitleRequest {
const { paneKey: _paneKey, ...forwarded } = request
return forwarded
}
Loading
Loading