Skip to content

feat(agent-launch): show an agent's tab at once, where the caller asked - #25430

Merged
brennanb2025 merged 6 commits into
mainfrom
brennanb2025/agent-launch-instant-tab
Oct 7, 2026
Merged

brennanb2025 merged 6 commits into
mainfrom
brennanb2025/agent-launch-instant-tab

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 32 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2560 $\color{#cf222e}{\Huge{\mathbf{−}}}$​9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2551
Prod 88 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2293 $\color{#cf222e}{\Huge{\mathbf{−}}}$​292 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2001

ELI5

When you start an agent from the phone, its tab used to appear only after Orca had finished a list of chores: writing down the launch, finding the workspace, deciding chat or terminal, and starting the process. On a Windows machine whose first PowerShell was still booting, that took about 1.7 seconds. Clicking a button in the desktop app shows its tab in 2–4 ms.

The tab also always landed in whichever split was active, not where you asked.

Now Orca shows the tab first, in the place you asked for, and then does the chores. The tab waits for the agent and connects to it when it starts. If the agent can't start, or Orca can't tell whether it started, the tab says so instead of turning into a plain shell, and it still says so after Orca restarts.

What Changed

Before: an agent.launch tab appeared only after the host spawned the agent. Measured on Windows (pwsh panes): 174 ms on a first launch, 66–80 ms warm, and 1,668 ms (1,555–1,697, 5/5 runs) when the click came ~4 s after the workspace's first PowerShell started. In that cold window the daemon's PTY create took ~1 s and opening the launch record ~0.55 s, and the tab waited for both. The desktop's own renderer-created tab: 2–4 ms in every case.

After: the host asks the window for the tab as its first act, before it admits the launch. In a local harness that replays the measured costs, the tab is shown at 31–33 ms on a first launch, cold window or not, and 9 ms warm (step 5 under the same harness: 1,660 / 172 / 88 ms). Only a launch that carries an operation id gets the early tab (the phone always sends one); see "The pane after its launch" for why.

Before: the tab went to the active split. A launch request had no way to say where.

After: a request can name a group, the tab to follow, or both. If the group was closed meanwhile, the tab goes to the anchor tab's group, then to the active one. Placement never fails a launch: an id that isn't a string, or a key this host doesn't know, is dropped. The receipt says where the tab landed and whether it fell back.

Before (and in this PR's first draft): a pane that mounted before its agent existed was guarded only by a marker in memory. A restart, an unknown outcome, a second launch naming the same pane, or a refused launch could each leave the agent's tab running a plain shell, calling a running agent "not started", or killing a replayed agent.

After — the pane after its launch: the launch record from step 5 names the pane the window showed, written in the same transaction that lets the launch run, and the tab keeps what that pane's launch comes to. Every time the pane's process would start, main works out its fate and tells the window as a typed value:

  • A process holds the pane, live or by its persisted binding → it attaches. A restored pane whose agent survived in the terminal daemon is adopted, not called unconfirmed.
  • The launch never started its agent (it failed before the agent's process was asked for) → "The agent couldn't start." with a reason in plain words when there is one ("Its command wasn't found on this machine.", "It exited while starting."), never a host error code. An agent that starts and then crashes, or whose command fails inside the pane's shell, shows its own output and the shell, as on main; the notice is for a launch that never got that far.
  • Nobody can tell whether it started (the spawn was asked for and the answer was lost) → "Couldn't confirm the agent started." Never shown as a failure, and a later process that holds the pane is still attached.
  • The launch ran elsewhere or never ran (a refused or replayed request) → the pane is taken back: the whole tab when that pane is all it has, only that pane when you split the tab while it waited. A pane already waiting is refused rather than given a shell.
  • No launch owns the pane, or its agent ran there and has since exited → an ordinary terminal, as before.

While the launch's fate is open, the pane reads the launch record. Once it is settled (the agent attached, or the pane is an ordinary terminal) the tab forgets the launch, so a restored tab never opens the record during startup. Once it is final ("couldn't start" or "couldn't confirm") the tab keeps it for as long as the tab exists, past the record's 24 h replay window, until you close the tab or a different launch reuses the pane; after that the pane reads nothing but its tab. A retry of the same launch (a phone resending it after a lost reply) is that launch: it shows no tab and changes nothing the tab keeps. Every shown pane that nothing spawns into ends in a verdict, so it never stays blank. A different launch into a pane that shows an earlier launch's outcome remounts that pane, so it waits for the new launch and attaches to its agent.

The message stays in the pane: it can't be dismissed into a blank pane, has no "file an issue" link, and offers Copy prompt while this session of the app holds the prompt (after a restart the message shows without it). A pane no launch laid out pays nothing.

Before: closing the early tab while its agent was starting didn't stop the launch, and the tab came back when the agent spawned.

After: your close wins. The host records every close you make, of a tab or of one pane in a split, from the window, the phone or the CLI, and a launch still starting or delivering its prompt there is stopped. The launch stops before it spawns the agent, or stops the agent if it already spawned, and its answer, to the caller and to every retry of it, is agent_launch_tab_closed; a retry shows no tab. The phone (which reads that word) says "The agent's tab was closed on the computer before it started, so it was stopped."; a caller that doesn't read it gets agent_session_operation_unknown, as it always did. A tab that is only missing (a window reload before the session saved it) is not a close: the agent keeps running and its tab comes back. A close that lands while the agent's terminal is being created is checked again as soon as it exists: the agent is stopped before any prompt is pasted. A prompt that rode the agent's command line has already reached it; that agent starts and is stopped a moment later (nothing is typed).

Before: a second launch that named a pane another agent was running in could mark that pane as failed.

After: a launch whose pane is already running gets no early tab and never touches that pane; the runtime refuses it as before.

Before: the window decided a launched tab's first view (terminal or chat view) on its own, without knowing whether the prompt was a draft.

After: the host decides it by the same rule the window uses for a tab you open yourself, with the prompt's delivery included, and sends it explicitly. It is never a caller field.

Whose screen moves is unchanged from main:

  • a phone (or other paired) launch still reveals the workspace on the desktop, as before, and also moves the phone's own selection to its tab;
  • a local caller without a presentation keeps today's reveal; asking for focus moves the window;
  • a background request moves nobody;
  • the desktop's own launches (step 7) select the tab in their workspace and move the window only if you are still on that workspace.

Before: the launch record was opened on the first launch, on the click path.

After: it opens ahead of the first launch only when it can pay off: once startup has settled (1.5 s after the window's first load, or after the RPC server starts in headless serve) and a client that can call agent.launch (a paired phone) is connected. A profile no such client uses never opens it early. Measured, the open blocks the main thread for 7–16 ms on a fresh profile and ~4 ms with 600 rows on an idle Mac, and 95–280 ms on the same Mac under heavy load (load average 24 on 18 cores). Admission still opens it on demand if this hasn't happened.

Mechanism:

  1. Publish first. agent.launch (with an operation id) resolves the workspace (cheap), checks the settings-only mode preflight, and, for a terminal launch with a window that owns the layout and a pane nothing is running in, registers the launch as running for that pane and sends ui:publishAgentLaunchTab with the caller's paneKey (or one it mints), the operation id, the host-derived view, the placement, the viewer rule and the prompt. An operation the record already holds (a retry, a conflicting reuse of its id, a recorded failure) publishes nothing: nothing new runs for it.
  2. The window creates the tab under the host's ids, reusing the one with that id if it already exists (a retry never makes a second tab; only a different operation resets what an earlier launch left on the pane), places it with the existing tab-placement rule, lays out the pane with no process, marks the tab agentLaunchPane: { leafId, operationId } (persisted), and replies with the group it landed in.
  3. Admission records the pane. The claim that lets the launch run writes ownedPane: { worktreeId, paneKey } on its ledger row in the same transaction; settling the row keeps it.
  4. The pane's spawn derives its verdict before it reserves anything: wait while a launch for it runs in this process until that launch's agent holds the pane (not until its prompt is delivered) or the launch ends, attach if a process holds the pane (live or by persisted binding), else the outcome the tab keeps, else the record. Main sends the verdict to the window (ui:agentLaunchPaneVerdict), which clears a settled one, keeps a final one on the tab, or takes a withdrawn pane back; a refused spawn fails with one fixed code.
  5. Finish. A tab this request made is taken back only when nothing will ever run in it (never admitted, or its surface landed elsewhere) and no process holds its pane; any other shown pane nothing spawned into gets the verdict the record now gives it. The spawn binds a tab the window said it showed without moving anyone again; if the window never answered, the spawn reveals the tab as before. The user's close of the launch's tab or pane, which main commits itself (the window's close commit, or main's own close of a pane a phone or the CLI asked for), stops the launch before its spawn, or stops its agent after, and records agent_launch_tab_closed.

Wire: two optional request fields, placement: { groupId?, afterTabId? } and presentation: 'focused' | 'background'; one optional receipt field, placement: { groupId, fallback? }; three capabilities, each one promise: agent.launch.placement.v1, host-advertised (reads placement and presentation, reports the placement, may publish the tab before admission); agent.launch.unstarted-tab.v1, client-advertised (reads a listed launch tab with no terminal as not started); and agent.launch.tab-closed.v1, client-advertised (reads the new refusal code agent_launch_tab_closed, which the RPC layer passes through as its own error code; others get agent_session_operation_unknown for it, as before). presentation is terminal.create's existing field with its meaning; whose view moves is decided by the host per caller and matches main for every existing caller, so no navigation field is added. Both request fields are outside the replay fingerprint and never refused: a value of the wrong type or a word this host doesn't know is dropped. Older hosts drop them. No protocol bump. The ledger row gains an optional ownedPane, which older builds keep as part of the row and ignore; the tab gains an optional persisted agentLaunchPane, which older builds drop.

Mixed versions: today's phone treats "my reserved tab is listed" as proof its agent started when a reply is lost. A tab listed before the spawn would break that, so the host shows a paired caller's tab early only when the caller advertises agent.launch.unstarted-tab.v1. This phone build advertises it, sends placement: { afterTabId } for the "+" launch, and treats a listed terminal tab as proof only once it has a terminal. Other phones viewing the workspace (not the caller) do see the early tab with no terminal yet, and a failed launch's tab stays listed with no terminal; neither phone build has its own rendering for "couldn't start" (live QA scheduled).

Why

The common pattern creates the surface first, under a caller-supplied or allocated id, starts the process after it, and records a start failure in that surface's own record, so it reads the same on every later view and after a restart, for as long as the surface exists. Only the client that created it moves its own view. This PR follows that for the terminal tab: tab first under the caller's pane id, process second, and the pane's final fate kept on the tab itself.

Creating the tab "alongside the spawn" instead would have left the cold-window admission cost (~0.55 s) in front of the tab; the measurement ruled it out as a fix on its own. Opening the record early alone leaves the ~1 s daemon create in front (harness: 1,117 ms). Keeping the outcome only in the launch record would have expired it with the record's 24 h replay window and made every restored launch tab open the record at startup; keeping the open question in the record and the final answer on the tab avoids both.

Differences from the common pattern:

  • Intended: the host picks chat or terminal, so the early tab is shown only when the settings preflight says terminal. Safe: a launch the preflight calls terminal can only end as a terminal, because the host's own check only ever downgrades a chat to a terminal.
  • Intended: a missing group falls back (anchor's group, then the active one) instead of refusing. Orca's splits are window layout that a remote caller cannot keep in sync; refusing would fail a launch the user can't fix.
  • Intended: the caller's tab id rides the existing paneKey (tab + pane) instead of a separate tabId field; two copies of one id can disagree, and the phone already sends paneKey.
  • Intended: while a launch's fate is still open, the pane asks the launch record rather than the tab, because only the record knows how the launch ended; the tab carries just which pane is waiting. Once the fate is final it lives on the tab, as in the common pattern.
  • Intended: Copy prompt is offered only in the app session that received the prompt. Keeping a caller's prompt on disk for a failure message would be a second copy of user content the caller still has.
  • Temporary: a host crash in the moments between showing the tab and the window saving it (live QA: under ~0.3 s), or before admission, leaves no restored tab or one no record names (an ordinary terminal). Follow-up: admit before publishing once the record is always open by the first click.
  • Temporary: when the record isn't open yet (the first launch after a restart, before a phone has connected), a request that admission then refuses still shows its tab for a few tens of milliseconds before it is taken back. Follow-up: the same admit-first change.
  • Temporary: a chat-mode launch still gets its tab when the session is created, after admission, and pays the workspace lookup first (~20–28 ms cold) to learn it is chat. Follow-up: publish a pending chat tab the same way; plan §6 lists it as a risk.
  • Temporary: a host with no desktop window (headless serve), a launch without an operation id, and a desktop client paired to a remote Orca still get the tab when the agent spawns. Follow-up: the same publish through the headless session-tab model, and step 7's callers send operation ids.
  • Temporary: a launch that creates its workspace keeps worktree.create's startup terminal, timing and view rule. Follow-up: step 7's workspace-create rework.
  • Follow-up (separate behaviour change, not in this PR): a phone or other paired caller's launch no longer moving the desktop to its tab. An earlier revision of this PR did that; it was moved out so this PR changes no one's screen compared with main.
  • Cost: on a first launch the agent starts ~20–28 ms later in the harness, because the workspace lookup now runs before admission instead of after it (0 ms warm).

Plan change: plan G1 called for an "agent started in workspace — Go to" notice when you have switched away. It is dropped under Brennan's UI rule (no notice the references don't show): neither reference shows one, and a launch you started elsewhere appears in the workspace's tab strip and sidebar, where the creating client highlights or navigates to it.

This is the second of three PRs that unify agent launching, rebuilt directly on main:

  1. feat(agent-launch): host-assigned caller identity and a launch record written when the tab exists (unification 1 of 3) #24934: launch identity and record (this PR's base)
  2. Placement and the instant tab (this PR)
  3. feat(agent-launch): the desktop AI buttons start their agent through agent.launch #25624: the desktop AI buttons go through agent.launch

Prompt delivery is main's (#24257: a short single-line prompt rides the command line, otherwise a guarded paste once the agent is proven in front). #24080 (the first prompt on the command line) and #24081 (worker briefs on the launch line) are parked and return later as their own PRs; this PR uses nothing from them.

Scope: the host's launch path up to the tab, the window's tab creation, the pane's verdict, the record's opening, and the phone's "+" request. No desktop caller uses agent.launch until step 7, and nothing in the orca CLI calls it yet.

Linked Issue

Part of the agent launch plan; no separate issue.

Visual Proof

N/A for this draft: the change is when and where a tab appears and what a pane says when its agent isn't running. Live before/after on Windows (cold window) and the phone is scheduled as QA; this PR does not launch the app.

Testing

  • Live QA (run by the stack coordinator on f04dea9dbb4, Mac zsh and Windows PowerShell, stub agents, a scripted paired-phone caller): tab before admission on every launch (Windows first launch 30 ms vs 122 ms on the base, warm 7 vs 56 ms); placement, per-viewer navigation, the user's split surviving a withdraw, a reload during the wait and the warm-up all passed. It found two problems, both fixed in b35fdb3ba43/2cddd5146d7 and not yet re-run live: a phone retry blanked a "couldn't confirm" pane (F6), and closing the tab during the wait answered "unknown" with a retry flashing the tab (F2). Phone rendering of an early or failed tab is still unverified (no simulator lane). A re-check on 2cddd5146d7 (M4Air, the phone's own launch code run headless) passed F6, F3 and F2's retry and found two more: the tab-closed answer reached the phone as runtime_error (F7), and a new launch into a "couldn't confirm" pane started its agent but left the pane blank (F8); both are fixed in 648a4d4ac51 and not yet re-run live. Live QA of the next step then found an early tab stayed blank until the prompt was delivered, because the pane's spawn waited for the whole launch: a dialog the agent showed at startup was invisible until the delivery timed out. Fixed in 6273cc5f508 (the pane attaches as soon as the agent runs); a live check on that head passed it (the agent drew at 1 s, a startup question was visible, the prompt landed once, and closing the tab stopped the agent). That check also found that the desktop window was told "unknown" when you closed the tab mid-delivery, where the CLI was told the tab was closed; since da3707a1cc0 the desktop window is told so too. A CLI launch into a just-created empty split landed in the first group, and the split disappeared (3 of 3 runs; main kept it). The cause: a CLI's focused launch made the window switch to the workspace after choosing the split but before creating the tab, and switching workspaces tidies away empty groups once any tab is live. Since 8bba067ff41 the window creates the tab in the split first and switches after, so the split stays and holds the tab; a live re-check on that head passed it 3 of 3. A Claude review of the last changes then found the design left a second guard: the window kept a copy of "a launch still runs in this pane" only to decide whether to report a close. Since 8c77f31c9d3 main hears the close from its own commit of it instead, which also stops a launch when you close just its pane in a split (before, only a whole-tab close did), and since 91329fd297e that covers a pane close the phone or the CLI asks for. The same lost-split order existed in a phone's focused "new terminal in this split"; since 05137dcc577 it creates first too, and a tab created this way still moves its workspace up in Recent. A live re-check on 91329fd297e passed them all (every close answers that the tab was closed and stops the agent, including a split-pane close from the window and from the CLI; the phone-style new terminal keeps its split where main loses it). A readiness pass then added 9473e9e0651 (a phone landing on the launch's early tab no longer starts a second agent in it) and 8f62858e606 (the phone's closed-tab wording, a closed pane's outcome, a new local worktree's chat view); both are covered by tests, and a live run on 8f62858e606 with a paired iOS Simulator passed the phone rows: a phone landing on its own pending tab got exactly one agent and no error, and closing that tab during start stopped the agent with no error shown. The new-worktree chat-view fallback was not reached live (the startup terminal always came up), so it is covered by its test only.
  • Automated tests added/updated

Local timing harness (the real agent.launchReplay handler and on-disk launch record, with the measured Windows segment costs injected: admission 554 ms cold / 52 first / 18 warm, workspace 23 / 0, spawn 1,063 / 83 / 59, window 7 ms; median of 5):

step 5 tab this PR tab step 5 spawn starts this PR spawn starts
cold window 1,660 ms 33 ms 583 ms 611 ms
cold window, record already open 1,117 ms 32 ms 45 ms 70 ms
steady first launch 172 ms 31 ms 81 ms 102 ms
warm 88 ms 9 ms 20 ms 24 ms

The step-5 column reproduces the live measurement (1,668 / 174 / 80 ms), which is what makes the harness worth reading. It is a model, not a live run: the live cold-window rows still have to be run.

Opening the launch record (real code, node:sqlite, a loaded 18-core Mac, 5 runs each): 142–282 ms on a fresh profile, 93–106 ms with 600 rows. That is why it now waits for the window's first load.

What the tests pin (each was checked by breaking the code and seeing it go red: 14 in review round 1, 14 in round 2, 12 for the live-QA fixes):

  • Before admission: the window is asked for the tab before the launch record is opened; the spawn lands in that tab's pane, including one the host minted.
  • The record names the pane: admission writes it with the claim; a lost claim doesn't; settling keeps it.
  • A failed spawn: the pane says it didn't start, the tab stays, and it says the same after a restart (record reopened from disk, nothing in memory).
  • An unknown outcome: "couldn't confirm", never a failure; a process that turns up, live or by the pane's persisted binding, is attached.
  • The tab keeps the fate: a final outcome is answered from the tab with no record read (so past the record's expiry); a retry of the same launch keeps it and a different launch resets it; a settled pane clears its tab, and a restored settled tab spawns without opening the record; every shown pane nothing spawned into gets a verdict. A different launch into a pane showing an outcome remounts it, and that pane's spawn waits for the new launch and attaches, never answering with the old outcome.
  • Typed verdict: the spawn reports it to the window and fails with one fixed code; the window clears, keeps, or takes the pane back.
  • A second launch naming a running pane: no early tab, the running agent's pane is untouched.
  • A refused launch: its tab is taken back and its waiting pane refused; a split the user added stays and only the launch pane closes.
  • Replays: no second tab; a retry of an operation the record holds shows no tab at all (no flash, no reset, no one moved); with the record not yet open, a tab still shown keeps what its launch came to, a tab remade for nothing goes, and one an agent survived in stays; a refusal that brings a fresh pane flashes nothing when the record holds its id.
  • Closing the tab during the wait: main hears it from its own commit of the close, for a whole tab or one pane, from the window, the phone or the CLI; the launch stops before its spawn (no process) or stops the agent that spawned, records agent_launch_tab_closed, answers it to a caller that reads it (the CLI, the phone, and the desktop window, which ships with its host) and agent_session_operation_unknown to one that doesn't, and a retry shows no tab; a tab merely missing after a reload comes back with its agent. A close during terminal creation stops the agent before its prompt is pasted. A pane attaches to its agent while the prompt is still being delivered, and a close then still stops the launch as agent_launch_tab_closed. The code reaches the caller on the wire as agent_launch_tab_closed (not runtime_error), which the phone's classifier reads as a definite failure.
  • Quiet reveal: only when the window said it showed the tab; otherwise today's reveal.
  • The notice: localized, plain reason, no host code, "couldn't confirm" for unknown, shown only on the pane the launch laid out.
  • Warm-up: the record opens only after startup settles and a client that can launch connects, never for a profile no such client uses.
  • Wire: non-string placement ids and presentation are dropped; unknown words read as absent; an older host's schema drops both; the phone advertises the client capability and the host gates on it.
  • Placement and navigation (window side, real store): requested group, then the anchor's group, then the active one; "stay put" when you moved to another workspace.

Not covered by an automated test: that the notice has no dismiss control (structural), the paired-client onReady hook that marks a launching client ready (the gate it feeds is tested), and the pane projection's use of the tab's outcome (the helper it calls is tested).

Checks on the rebuilt head (ea4a899fb2e, one commit on #24934's rebuild 82544cbb3df, which is one commit on main 842c6c667a8):

  • Rebuild: feat(agent-launch): show an agent's tab at once, where the caller asked #25430's own diff (7d98ac4c634..8f62858e606, whose merges only brought in feat(agent-launch): host-assigned caller identity and a launch record written when the tab exists (unification 1 of 3) #24934 heads) applied onto the new base with a 3-way apply; one conflict, an import line in terminal-tab-close.ts (both kept). Plus the move-out above (2 lines and its tests).
  • Typechecks: tc:node, tc:web, tc:cli pass.
  • Root named-file run (local): every test importing a changed module, every useIpcEvents test, orca-runtime.test.ts, and the terminal store, terminal pane, pty IPC, agent-launch and runtime RPC trees: 2,347 files, 23,931 tests passed, 0 failed.
  • Cross-version wire suite (local): 25 files, 132 passed.
  • Mobile (local): 15 files, 187 passed.
  • Lint: oxlint and oxfmt on changed files clean; the changed-lines quality gate, React Doctor, and the max-lines, runtime-Electron, ts-nocheck and reliability-gate checks pass.
  • Live QA on this head (round 6, vs current main): the blank pane (agent drawn by 1 s, a 600-byte prompt pasted once), closing the tab mid-delivery from the desktop and the CLI (agent_launch_tab_closed, agent stopped), whose screen moves for a phone launch (identical to main) and Windows tab timing (tab in store 33 ms first / 6 ms warm vs main's 156 / 62 ms) all pass. Rounds 1–5 ran on the earlier stack-based heads.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: the tab is created by the local window for an SSH workspace too; the pane's verdict is keyed by workspace and pane and reads the same record, so it covers SSH panes. Not live-tested.
  • Windows: no platform-specific code; the cold-window rows are the ones to run live.
  • Mobile: an old phone launching is unchanged (no early tab). A new phone on an old host sends placement, which the old host drops. Phones viewing a workspace see an early or failed launch tab with no terminal (see Mixed versions).
  • Folder workspaces and the floating workspace: same path; the early publish is skipped only where the existing launch already differs (new workspace, reused terminal).
  • Live rows still needed: the pane's attach when the host's reveal lands while the pane's own spawn is waiting (hidden window, pwsh, SSH).

Author: @BrennanKB5

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@brennanb2025
brennanb2025 marked this pull request as ready for review October 5, 2026 15:16

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

One real edge in the withdrawn-verdict path can close a pane the user still has: when the launch pane's leaf has already left the layout, applyAgentLaunchPaneVerdict mistakes the user's remaining single leaf for the launch pane and closes the whole tab. Two smaller reuse-related stale-state points are inline.

Reviewed changes

Step 6 of 7 of the agent-launch plan, stacked on #24934. It publishes an agent.launch terminal tab to the window before admission, then derives each launch pane's fate from the durable operation ledger and keeps the final answer on the tab. I read the full 98-file diff.

  • Main publish/verdict pipeline — new agent-launch-tab-publication.ts (publish before admission; take back only a tab nothing will run in), agent-launch-pane-attachment.ts (in-process running marker + record-backed verdict), and spawn-begin.ts gating a launch pane's spawn on that verdict.
  • Wire — tolerant placement/presentation request fields, a placement receipt, host agent.launch.placement.v1 and client agent.launch.unstarted-tab.v1 capabilities; both fields excluded from the replay fingerprint, no protocol bump.
  • Ledger — ownedPane written with the winning claim and kept through settle.
  • Window — tab creation under host ids with placement/fallback, agentLaunchPane on the tab, the pane notice, and the user-close-wins reveal.
  • Persistence — agentLaunchPane survives restart; malformed values are dropped without failing the session.
  • Mobile — the "+" launch sends afterTabId, advertises the capability, and no longer reads a terminal-less listed tab as a started agent.

Confirmed out of scope after checking: ordinary same-caller retries are coalesced by runReplaySafeAgentLaunch before publishEarlyTab, so the runningLaunchesByPane overwrite is not reachable through that path; agentLaunchPane does persist and restore; placement fallback is correct.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

case 'withdrawn': {
forgetAgentLaunchPanePrompt(tabId)
// The pane alone when the user split the tab while it waited; their split stays.
if (state.terminalLayoutsByTabId[tabId]?.root?.type === 'leaf') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withdrawn branch closes the whole tab whenever the current layout root is a single leaf, but it never checks that leaf is leafId. If the user split the tab while the launch waited and then closed the launch pane, the root collapses to their leaf and this closes that tab too — contradicting the comment that their split stays. Closing a pane leaf does not clear agentLaunchPane, so the guard above still passes.

Technical details
# withdrawn closes the user's remaining pane

## Affected sites
- `src/renderer/src/lib/agent-launch-pane-verdict-application.ts:33` — `root.type === 'leaf'` is read as "the tab holds only the launch pane", but after a pane close the single leaf is whichever leaf survived (the user's).

## Required outcome
- Close the whole tab only when the single remaining leaf is `leafId`. When `leafId` is no longer in the layout, clear `agentLaunchPane` and leave the user's pane; when `leafId` is present in a split, keep today's `applyClosedTerminalLeafNotice(tabId, leafId)` behaviour.

## Open questions for the human
- For a `withdrawn` whose `leafId` is already gone from the layout, should anything be emitted at all?

}
}

export function wasAgentLaunchPaneClosedByUser(tabId: string, leafId: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasAgentLaunchPaneClosedByUser only reads the set; nothing ever clears a mark, and the key is (tabId, leafId) with no operation scope. A later launch that reuses that pane key — callers may supply a stable paneKey, and the renderer explicitly re-arms agentLaunchPane on reuse — would have its reveal killed as if the user had closed it. Clearing the mark when a new launch arms the pane, or scoping it to the launch, keeps "close wins" from leaking onto a different launch.

rememberAgentLaunchPanePrompt(tabId, request.prompt)
}
// A new launch into this pane: whatever an earlier launch left on it no longer stands.
store.setTabAgentLaunchPane(tabId, { leafId })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reuse branch re-arms agentLaunchPane but only overwrites the remembered prompt when the new request carries prompt. A new launch into the same pane with no prompt leaves the previous launch's text in promptsByTabId, so a failed launch can offer to copy another launch's prompt. Clear the remembered prompt here before optionally remembering the new one.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live QA status: base #24934 (b2c513f9e32) vs this PR (f04dea9dbb4)

Summary:

  • The instant tab and placement work: the tab now shows 4–10× sooner, and placement, per-viewer navigation, idempotency, reload and withdrawn launches all pass.
  • Three behaviours a user would notice need a look before ready: F2 and F6 below are new with this PR; F1 is the same as base.
  • Not verified: the hooks-on cold window and the phone UI.

Setup:

  • Hidden rig apps with isolated home and user-data directories. Stub agents only, agent status hooks off.
  • Caller: a scripted paired client using the phone's transport and capabilities. It sends agent.launchReplay the way the phone's "+" does: an operation id, a minted paneKey, and placement: { afterTabId }.
  • Windows high spec (PowerShell and cmd) for timing; this Mac (zsh) for timing and every behaviour row.
  • Host segments come from local-only logpoints applied identically to both arms; nothing was committed. Rows that need a user action during the wait hold the host for ~1.5 s with a local-only pause.
Row Base This PR Verdict
1. Tab visible, Windows PowerShell, click 4 s after the first PowerShell started, first launch (n=5) 122 ms (119–128) 30 ms (29–33) faster; not ≤ 2–4 ms on a first launch (~21 ms is the workspace lookup before the publish)
1. Same, warm (n=10) 56 ms 7 ms ✅
1. Mac, first / warm 49 / 19 ms 17 / 5 ms ✅
1. Ordering (30 Windows + 15 Mac PR launches) tab waits on admission, record open, spawn, daemon create tab requested before admission, admission before spawn, tab in store before the daemon create, every time ✅
1. Hooks-on cold window (1.6 s in the earlier measurement) not reproduced with hooks off (base 119–128 ms) — unverified live; the PR's tab is independent of it by the ordering above
2. Placement: group / after-anchor / gone group + anchor / gone group always the active group as requested; fallback: 'anchor-group' / 'active-group' in the receipt; every launch ran ✅
3. Desktop switched workspace mid-launch; two phones desktop stays; caller selected same; the tab is in the target workspace at 19 ms; the other phone is unchanged; no notice ✅
4. Agent crashes at start / command not found plain shell, no notice plain shell, no notice (verdict proceed) ❌ against the row's expectation, same as base (F1)
4. Host killed before the spawn answered, restart no tab "Couldn't confirm the agent started.", never a shell ✅, then ❌ after a phone retry (F6)
4. Agent survived a host kill, restart adopted adopted ✅
5. Same op + pane ×4; op without pane ×3; refusals without pane 1 tab, 1 agent, no flash 1 tab, 1 agent, no flash ✅
5. Refusal with a fresh pane no tab its tab flashes ~37 ms, then goes by design; visible (F3)
6. User closes the tab during the wait — stays closed, no agent ran ✅, but the reply is agent_session_operation_unknown (F2)
6. Window reload during the wait — the tab comes back with its agent ✅
7. Refused launch whose tab the user split — only the launch pane closed; the user's split and tab stayed ✅
8. Record warm-up opens on the first click (9–33 ms in the click path) opens after startup once the phone connects: 25 ms, no main-thread stall ≥15 ms (fresh); 3 ms (existing) ✅
9. Phone view of an early / failed tab not listed listed as pending-handle, terminal: null; a "couldn't confirm" tab stays so indefinitely data ✅; phone UI blocked (no PR mobile build on a simulator)

Findings a user would notice

F1. "The agent couldn't start." never appeared in a live failure.

  • On macOS the agent runs inside the pane's shell. An agent that exits at once, or whose command is missing, leaves a plain shell in the tab on both arms.
  • The notice covers only failures before the PTY spawn is dispatched, and I found no live trigger for that. A workspace folder removed mid-launch ends as "Couldn't confirm".
  • Not a regression, but the row's expectation of "never a plain shell" is not met.

F2. Closing the tab while the agent starts returns agent_session_operation_unknown to the caller.

  • The tab stays closed and no agent runs, as described. But the description says the launch still reports the terminal it created.
  • The main log shows failed to persist runtime PTY binding after spawn: terminal_split_source_not_found, and the request fails with unknown.
  • Retrying that operation also returns unknown and re-shows the closed tab for ~37 ms.

F6. A phone retry erases the "Couldn't confirm" notice and leaves a blank pane.

  • After a host restart, the restored tab correctly says "Couldn't confirm the agent started.".
  • When the phone then retries the same operation, the host answers unknown, but the tab's kept outcome is cleared. The pane is left blank: no notice, no shell, no agent. It was still blank 70 s later. Seen in 3 of 3 runs.
  • From the code (not traced live): the retry republishes onto the existing tab (created: false), which resets what the earlier launch left on it. Admission then refuses, and nothing sends a new verdict, because only a tab the request created is taken back.

F3. A refused retry carrying a new pane flashes an empty tab for ~37 ms.

F4. A host crash within ~0.3 s of the publish loses the early tab across a restart, because it had not been saved yet. The user sees no tab and no agent.

Screenshots

Captured from the hidden rig window over the DevTools protocol. "Grok" means an independent read-only driver captured and described it; "CDP" means a scripted capture.

Row 1, spawn held: on base there is no launch tab yet (CDP); on this PR the launch tab ("Terminal 3") is already in the strip, after the anchor (Grok).

base before spawn
PR before spawn

Row 2, placement: after-anchor and requested-group launches (Grok).

PR placement

Row 4:

  • A crash at start leaves a plain shell (Grok).
  • "Couldn't confirm" after a restart (Grok).
  • The same pane after a phone retry, blank (Grok).
  • "Couldn't confirm" + Copy prompt before a restart (CDP).

PR crash shell
PR unconfirmed after restart
PR blank after retry
PR unconfirmed with Copy prompt

Row 6: the tab was closed while the agent started; no tab came back and no agent ran (Grok).

PR closed while starting

Not verified

  • The hooks-on cold window.
  • The phone UI.
  • Behaviour rows on Windows (Windows ran timing only).
  • SSH, WSL and folder workspaces; real agents; packaged builds.

Safety

  • Real agent configs: unchanged on Windows throughout. On this Mac, ~/.codex/config.toml changed once at 07:11, outside any rig run. The writer is not established; the file was not read, fixed or restored.
  • Isolation: every stub recorded a home inside its rig directory.
  • Processes: every rig was stopped by its recorded PID; final scans are clean on both machines.

@brennanb2025
brennanb2025 marked this pull request as draft October 5, 2026 15:58
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…b is the launch's answer

Live QA of #25430.

F6: after a restart showed "Couldn't confirm the agent started.", a
phone retry republished onto the tab, the window reset its kept verdict
and nothing re-sent one, leaving a blank pane.
- No early tab for an operation the record already holds: a retry of the
  same launch, a conflict or a recorded failure shows nothing (also F3's
  flash when the record is open).
- The tab records which operation laid out its pane; only a different
  operation resets what an earlier launch left on it.
- Every shown pane nothing spawned into ends in a verdict: taken back,
  or what the record now says.

F2: closing the tab while the agent starts answered
agent_session_operation_unknown, and a retry flashed the tab.
- The window reports the explicit close to the host. The launch stops
  before its spawn, or stops the agent that spawned, and records
  agent_launch_tab_closed as its answer for the caller and every retry.
- Callers advertising agent.launch.tab-closed.v1 (this phone build) get
  that word; others keep agent_session_operation_unknown.

Also moves the execution-outcome helpers out of agent-launch.ts.
@brennanb2025

brennanb2025 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Live re-check of the QA fixes on 2cddd5146d7 (base #24934 b949d49dbec). Run on the M4Air with stub agents and a scripted paired-phone caller. Report only.

Head moved during the run: it is now 3ba98396a84, a merge of main and the stack below this PR. errors.ts is unchanged there, so F7 stands. The launch and pane files differ only by two small terminal-state edits, not re-run live.

Two new problems:

  • F7: when the user closes the agent's tab on the computer, the phone shows the raw code agent_launch_tab_closed.
    • A caller that advertises agent.launch.tab-closed.v1 gets code: "runtime_error", message: "agent_launch_tab_closed". The code is in neither list of error codes the RPC layer passes through in src/main/runtime/rpc/errors.ts (RUNTIME_PASSTHROUGH_CODES, AGENT_SESSION_RPC_ERROR_CODES), so it is replaced with runtime_error.
    • Fed that live reply, the phone's own launchAgentInExistingWorkspace returns { kind: 'failed', message: 'agent_launch_tab_closed' } instead of "The agent's tab was closed on the computer before it started, so it was stopped." (4/4).
    • The unit test checks the error inside the method, not what goes over the wire. Old-style callers correctly get agent_session_operation_unknown.
  • F8: a new operation in a "Couldn't confirm" pane starts its agent, but the pane stays blank.
    • The notice goes, and the agent starts and stays alive; the host's copy of that terminal's screen shows its output.
    • But the pane never asks to attach (no pty:spawn within 4 s, 3/3 runs), so it is drawn blank, even after switching tabs and back. The tab also keeps agentLaunchPane with the new operation's id.
    • An ordinary launch on the same rig draws normally. The phone mints a fresh pane per launch, so this needs a caller that reuses a pane.

F8: agent running, pane blank
control: ordinary launch draws the agent

Fixed live:

  • F6: after a host crash, the restored tab's "Couldn't confirm the agent started." survives three phone retries, each answered agent_session_operation_unknown. No blank pane, no shell, no new tab, and the desktop doesn't move (3 runs, 9/9 retries).
  • F2: closing the tab while the agent starts keeps it closed and leaves no agent running, before or after the spawn (8/8). A retry shows no tab, not even for a frame (8/8).
  • F3: a refusal with a fresh paneKey, for an operation the launch record already holds, puts nothing on screen: 0 tab changes in the store or DOM (6/6).
  • Regression rows pass: placement and its fallbacks; reload during the wait; withdraw (only the launch pane closes); tab timing (the PR was faster in all 6 pairs, n=1 on a loaded machine).

F6: notice kept after three retries

Observation: a close that lands inside terminal creation still lets the agent process start, and it is stopped a moment later. The close check sits just before terminal creation (agent-launch-surfaces.ts:131). My hold widened that window; normally it is a few milliseconds.

Not run: Windows, the phone's UI (still blocked), the hooks-on cold window. The real agent configs on the M4Air were unchanged.

brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…ches a "couldn't confirm" pane

Live QA re-check of #25430.

F7: agent_launch_tab_closed was not in the RPC passthrough codes, so a
caller that reads it got runtime_error with the word in the message and
the phone showed the raw code. Pass it through; the only new code this
stack sends a client.

F8: a new operation into a pane that showed "couldn't confirm" started
its agent but the pane, whose spawn had been refused, never spawned
again. A different launch reusing a pane with an outcome now remounts
it (the tab-generation seam), and a pane that waited on a launch here
reads the record, not the tab's saved outcome from an earlier launch.

Race: a close landing while the agent's terminal is created is checked
again right after creation, so the agent is stopped before any prompt is
pasted. A prompt carried on the command line has already reached it.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live check of 648a4d4ac51 (F7, F8, close race): all pass, no new findings. This was a rig run on a Mac (M4Air) with stub agents, not CI. It covered the PR only, in 6 runs.

F7, fixed. If you close the tab while the agent is starting, a phone that advertises agent.launch.tab-closed.v1 now gets the code agent_launch_tab_closed (8/8). Before this fix it got runtime_error. The phone's own launch code now shows "The agent's tab was closed on the computer before it started, so it was stopped." An older caller still gets agent_session_operation_unknown (4/4). Retrying a closed launch gives the same answer, and the tab doesn't flash back (12/12).

F8, fixed. Setup: the host is killed mid-launch, then restarted, and the pane shows "Couldn't confirm the agent started." Three phone retries of that operation leave the notice in place (9/9).

  • A new operation in that pane now attaches: the window asks for the PTY 116–143 ms after the request, the notice clears, and the pane draws the agent's screen (3/3; Grok's screenshot matches the host's copy of the terminal).
  • Retrying the old operation afterwards changes nothing: same PTY, no tab changes, no new agent, and the running agent gets no input (6/6).

Close race. I closed the tab while the launch was held at four points: before createTerminal, before the spawn, after the spawn, and after createTerminal returns. Both callers were tested. At every point the tab stayed closed, no extra tab appeared, and no agent was left running (2 runs).

  • At the two later points the agent had already started and was then stopped, with 0 bytes pasted into it.
  • The stub's prompt rides its command line, so those agents did receive the prompt as an argument. That matches the commit message.
  • An agent whose prompt is pasted was not tested.

Sanity. A normal launch plus two warm ones all succeeded; Grok's screenshot shows the agent drawn. The tab reached the store in 15 ms (warm: 3 and 3), and the reply came back in 59 / 50 / 50 ms. The re-check's numbers for the same scenario were 103 (warm 7 / 5) and 343 / 263 / 115 ms. This is n=1, and the machine was less loaded this time.

Not covered: Windows, real agents, packaged builds, SSH and folder workspaces, and the phone's screen (F5 is still blocked).

Real agent configs were hash-checked before and after every run. ~/.claude/settings.json and the Codex files were unchanged. ~/.claude.json changed once mid-run. That machine has other live Claude sessions that write to it, and every stub agent ran with HOME, CODEX_HOME and CLAUDE_CONFIG_DIR inside its rig directory, so I haven't attributed the change; the file was not read or restored.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 5, 2026 22:36

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The four live-QA fixes in the new commits look correct, but the earlier finding that a withdrawn verdict can close a pane the user still has remains unaddressed.

Reviewed changes

This run re-reviewed the delta since the prior pullfrog review at f04dea9dbb: the three authored fix commits (b35fdb3ba4, 2cddd5146d, 648a4d4ac5) plus a base merge of #24934 that carries no PR-authored change.

  • A retry never resets its launch's tab — isRecordedOperation now suppresses the early tab for any operation the record already holds; the tab stores its operationId, and only a different operation resets what an earlier launch left on the pane.
  • A closed tab is the launch's answer — the window reports a user close over IPC; the host stops the launch (or the agent that spawned) and records agent_launch_tab_closed, which reaches only callers advertising agent.launch.tab-closed.v1 (and the CLI). Every shown pane that nothing spawns into now ends in a verdict.
  • A new launch reattaches a "couldn't confirm" pane — a different operation reusing a pane with an outcome remounts it via a generation bump, and settleVerdict ignores the tab's stale saved outcome once it has waited on a launch here, so it reads the record's fresh row.
  • Runtime boundary — the close-report listener moved into register-handlers, keeping electron out of the runtime graph.
  • Wire capability agent.launch.tab-closed.v1, the mobile wording of the code, and the RUNTIME_PASSTHROUGH_CODES entry all line up with the existing capability-gated pattern.

The prior review left three open threads (the withdrawn edge below, plus the never-cleared closedPaneKeys and the stale remembered prompt on a promptless reuse); none are touched by this delta.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

case 'withdrawn': {
forgetAgentLaunchPanePrompt(tabId)
// The pane alone when the user split the tab while it waited; their split stays.
if (state.terminalLayoutsByTabId[tabId]?.root?.type === 'leaf') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withdrawn branch closes the whole tab whenever the current layout root is a single leaf, but it never checks that leaf is leafId. If the user split the tab while the launch waited and then closed the launch pane, the root collapses to their surviving leaf and this closes that tab too, contradicting the comment that their split stays. Closing a pane leaf does not clear agentLaunchPane, so the guard at line 16 still passes.

Technical details
# withdrawn closes the user's remaining pane

## Affected sites
- `src/renderer/src/lib/agent-launch-pane-verdict-application.ts:33` — `root.type === 'leaf'` is read as "the tab holds only the launch pane", but after a pane close the single leaf is whichever leaf survived (the user's).

## Required outcome
- Close the whole tab only when the single remaining leaf is `leafId`. When `leafId` is no longer in the layout, clear `agentLaunchPane` and leave the user's pane; when `leafId` is present in a split, keep today's `applyClosedTerminalLeafNotice(tabId, leafId)` behaviour.

## Open questions for the human
- For a `withdrawn` whose `leafId` is already gone from the layout, should anything be emitted at all?

brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…top-buttons

The host now sends agent_launch_tab_closed as its own code; a launch it
refuses because the user closed the tab stays silent, as a closed tab does.
@brennanb2025
brennanb2025 marked this pull request as draft October 6, 2026 07:22
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Back to draft: live QA of step 7 (#25624) found that a terminal tab shown early by this PR stays blank until the prompt is delivered (in agent-launch-tab-publication.ts, the pane's running.finished resolves only at the end of the whole launch). With status hooks off or over SSH that's 5–60+ s of an empty pane, and a startup dialog the user must answer can't be seen. Main draws the agent at once. The fix (release the pane as soon as the agent's terminal is bound to it) is being made here, with a test, before this goes ready again.

brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…top-buttons

An early tab's pane now attaches when its agent runs, not after its prompt
lands (QA F1 on #25624).
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…top-buttons

The desktop renderer reads agent_launch_tab_closed, so a launch whose tab
the user closed mid-delivery answers that code and the window stays silent.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
@brennanb2025
brennanb2025 marked this pull request as ready for review October 6, 2026 14:59
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live QA (rounds 1–4) and review: summary before ready

What this PR is for. When you start an agent from the phone or the CLI, Orca used to show its tab only after it had finished a list of startup chores. That took up to 1.7 s on a cold Windows machine. Now the tab appears first, where you asked for it, and connects to the agent when the agent starts. If the agent can't start, or Orca can't tell whether it did, the tab says so instead of turning into a plain shell.

What live QA found, and what changed

All rows ran on the M4Air with stub agents (no real agent CLIs), comparing main with this PR. Rounds 1–4 are in order.

  1. The new tab stayed blank until the prompt was delivered.
    • Cause: the pane waited for the whole launch, prompt delivery included, so a startup question the agent asked (folder trust, an update prompt) was invisible until delivery gave up.
    • Now: the pane connects as soon as the agent runs. Live: the agent drew at 1 s, the startup question was visible, and the prompt was pasted exactly once.
  2. Closing the tab while the prompt was still being delivered told the desktop window "unknown", where the CLI was told the tab was closed. Now every caller that ships with this host (the desktop window, the CLI and the phone) is told the tab was closed, and the agent is stopped.
  3. A CLI launch into a split you had just created landed next to Terminal 1, and the split disappeared (3 of 3 runs; main kept it).
    • Cause: the window switched to the workspace before creating the tab, and switching workspaces tidies away empty groups.
    • Now: the tab is created first. Live: it lands in the split, 3 of 3.
    • The same order existed in the phone's "new terminal in this split". That is fixed too, and live it now keeps the split where main loses it.
  4. Closing just the launch's pane in a split never reached the launch, which then reported success with its agent killed.
    • Now: the host learns of every close you make, of a tab or one pane, from the window, the phone or the CLI, through the close it already records. A launch still starting or delivering there stops.
    • Live (round 4): a split-pane close from the window and from orca terminal close both answer "tab closed", the agent stops, and the other pane survives.

What review changed (two Claude review rounds plus a readiness pass)

  • Close handling: rebuilt rather than guarded. The window used to keep a persisted copy of "a launch is still running in this pane" only to decide whether to tell the host about a close. Keeping that copy alive through prompt delivery needed two extra rules. Now the host derives it from its own record of the close, and the copy, both rules and a separate close channel are gone.
  • A phone landing on the new tab no longer starts a second agent in it.
    • Why it could: the tab is listed before its agent spawns. Opening a pending agent tab on the phone made the host start that agent in the pane, which raced the launch: a prompt-less agent could take the pane, and the launch then failed as "pane already live".
    • Now: the host leaves a pane that a launch still owns to that launch. A test reproduced the race before the fix.
  • The phone's wording. The phone said "closed on the computer before it started" as an error, even when the phone itself closed the tab, or the agent had already started. A new tab's launch now ends quietly when its tab is closed; the PR AI button says "The agent's tab was closed, so the agent was stopped."
  • Smaller fixes:
    • a stored outcome is no longer kept for a launch pane you already closed;
    • a launch that creates a local worktree and falls back to a terminal now opens OpenCode, Grok or OMP in chat view like any local workspace;
    • a duplicate stop of an already-stopped terminal no longer logs an unhandled error;
    • a tab created this way still moves its workspace up in Recent.

Verified

  • Live, rounds 1–4: every row in LIVE-BLANK-PANE.md passed on its head. That covers the blank pane, close mid-delivery and during start, split-pane close (window and CLI), CLI placement, and the phone-style new terminal (sent by the CLI caller).
  • Every fix has a test that fails without it.
  • Typechecks pass for node, web, cli and mobile, plus the mobile test-typecheck ratchet.
  • Named-file sweeps pass:
    • 1,989 files, then 276 and 307;
    • 1,762 in round 4;
    • 55 + 6 mobile files after the last fixes.
    • In round 4's sweep, two git-provider CLI probe files timed out on a loaded machine. They import nothing this PR touches, and CI's test shards pass.
  • Lint, the code-quality gate and the ratchets pass.

Not verified

  • A close that lands strictly before the agent spawns. On the M4Air the agent spawned 0.14 s after the tab appeared, so every close came after. The before-spawn path is covered by tests only.
  • A real paired phone. None was available, so mobile QA is blocked. That leaves untested: the phone landing on the early tab (the race fix above), the phone's quiet close, and a phone's pane close.
  • SSH and remote workspaces.
  • The last two commits (9473e9e0651, 8f62858e606, from the readiness pass) are covered by tests but haven't been re-run live.

Known red check

cross-version wire compatibility, and the verify roll-up that reports it, fail on an older release (v1.4.221) that imports a package (@streamparser/json) this stack's base doesn't install. Main does install it. This clears when the PR is retargeted to main after #24934 merges.

Deferred (tracked, not in this PR)

  • Two launches that name the same caller-chosen pane can override each other's tracking. Rare, and pre-existing.
  • On phones, a launch tab that failed to start shows as a pending spinner, and tapping it starts a bare agent.
  • The "tab closed" answer isn't advertised to remote hosts the desktop talks to. It only matters if the desktop calls agent.launch on a remote host.
  • After a rollback, an older host answers a recorded "tab closed" retry as a generic error. This is bounded by the record's 24 h lifetime.
  • Stopping a launch uses a flag the launch checks rather than a cancel signal. Planned as a follow-up.

brennanb2025 added a commit that referenced this pull request Oct 6, 2026
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…acts, not on the executor's surface

CI lint, after merging #25430's final head: agent-launch-executor.ts was 301
lines. The launch time only travelled through the executor so the startup
check could read it back; it is a fact about the terminal the surface
factory just created, like the host facts it already keeps per handle. The
factory now keeps both in one record, and the executor is #25430's again.
… written when the surface exists

Step 1 of the agent-launch unification, on main.

- The dispatcher stamps every request's caller from what its connection proved (runtime socket:
  the local CLI; the desktop's IPC: the desktop; a paired socket: its device). Params never set it.
- The launch record is written twice: once when the tab exists (what creation settled: on the
  launch command, a draft, or a submit still `unconfirmed`), and again once the prompt's fate is
  known. A restart in between finds the running agent instead of answering "unknown".
- A replay re-derives its terminal handle from the pane key in the running host, and shows
  `unconfirmed` only to callers that advertise agent.launch.prompt-unconfirmed.v1.
- The record store opens in its own slot, without building the chat host; the chat host is built
  on that same store.

Rebuilt from this PR's own commits (b9adf88, 0d7d9b5, c83f44d, 39d15d5) onto
main, without #24080/#24081. Conflicts: the delivery doc table (main's "line fits" row plus the
`unconfirmed` row), and main's journal-database open in install(), which now goes through the
record-store slot.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/agent-launch-identity-record branch from 7d98ac4 to 82544cb Compare October 6, 2026 17:34
…ed for it

An agent.launch now shows its terminal tab before admission and spawn, in
the requested placement (group and/or anchor tab), and the pane attaches to
the agent as soon as it runs. A pane whose agent can't start, or whose start
can't be confirmed, says so instead of becoming a plain shell, and keeps
saying so across restarts. A user's close of the tab or its pane during the
launch stops it and answers agent_launch_tab_closed. Whose view moves is
unchanged from main for every caller.

Rebuilt on main (with #24934) from the previous branch head 8f62858.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/agent-launch-instant-tab branch from 8f62858 to ea4a899 Compare October 6, 2026 17:53
@brennanb2025
brennanb2025 marked this pull request as draft October 6, 2026 17:54
@brennanb2025
brennanb2025 marked this pull request as ready for review October 6, 2026 19:56
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live re-check on the main-based rebuild (ea4a899fb2e) vs current main

This PR was rebuilt directly on main with #24934. Prompt delivery is now main's own: a long prompt is pasted once the agent is ready. So the rows that depend on delivery or on whose screen moves were re-run, with stub agents, in hidden isolated apps.

Row main This PR Verdict
Blank pane, 600-byte prompt (pasted) tab when the agent starts; pasted once tab at 0.2 s, the agent drawn by 1 s; pasted exactly once after ready (6.96 s); receipt "handed to terminal" PASS
Close the tab mid-delivery, desktop and CLI callers "ok", prompt not delivered; agent stopped both told agent_launch_tab_closed; agent stopped; nothing left running; no error shown PASS
A phone launch: whose screen moves the desktop stays on its workspace; the launch's workspace is revealed in the sidebar identical PASS (unchanged from main, as intended)
Windows tab timing, PowerShell and cmd, 10 runs per arm (tab in store, median) 156 ms first launch / 62 ms warm 33 ms / 6 ms; agent start unchanged PASS

Placement, the phone landing on its own pending tab, and the phone's quiet close carry over from earlier rounds. CI is green on this head, 28/28.

Not verified: a real paired phone on hardware (an iOS Simulator through orca emulator was used), SSH workspaces, and real agent CLIs (stubs only).

@brennanb2025
brennanb2025 changed the base branch from brennanb2025/agent-launch-identity-record to main October 6, 2026 21:25
…t-launch-instant-tab

Conflicts resolved against #24934's rebuild (82544cb), which this branch was built on, so each
keeps main's squashed #24934 plus this branch's change. agent-launch-floating-workspace.test.ts takes
main's #22302 routing (a structured preference opens a Codex chat) with this branch's host-derived
viewMode on the terminal branch. The record-store slot test gets the resolveLaunchArgs dep #25721 made
required, the same line as main's fix.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9ac54b2d-7e32-448a-bdee-5c48fb6a7e3e
📥 Commits

Reviewing files that changed from the base of the PR and between b91abd1 and 9326aae.

📒 Files selected for processing (1)
  • src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The changes add early tab publication for eligible agent launches, placement and presentation fields, and launch-pane outcome tracking. The host and renderer exchange publication requests and pane verdicts. PTY spawning now checks pane evidence before proceeding. Closed-tab outcomes are handled by the host, renderer, and mobile clients. The changes also add native-chat initial view selection, record-store warmup, and localized launch-pane notices.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 9326a

No actionable merge-blocking risk was identified in the selected change. The author also reports passing checks and live validation for the main agent-launch behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 96 functions across 50 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: agent tabs appear immediately and use the caller’s requested placement.
Description check ✅ Passed The description is detailed and covers the change, rationale, testing, visual-proof status, and relevant compatibility considerations. The linked-issue section does not provide an issue link, and the …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

brennanb2025 added a commit that referenced this pull request Oct 6, 2026
…bs runtime stays under its line limit

The merge of main (#25724's emitMobileSessionTabsSnapshot metadata) plus this branch's close mark put
the file one line over max-lines. Formatting only.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
@brennanb2025
brennanb2025 merged commit 00e3027 into main Oct 7, 2026
136 of 142 checks passed
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
Jinwoo-H added a commit that referenced this pull request Oct 7, 2026
…ect (#26028)

#25430 added a test that opens a real agent-session record store, but not to the
SQLite runtime list, so vitest-sqlite-runtime-boundary fails on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant