@@ -263,6 +263,38 @@ the way down. Only once every other collapsible zone ahead of it in
263263` COLLAPSE_ORDER ` and the panel itself are exhausted does it reach 0, the
264264same last-resort floor every other optional zone shares.
265265
266+ ### Unprompted fleet reports
267+
268+ The agents panel is a standing picture of what is running right now; it says
269+ nothing when a lane finishes, stalls, or fails unless the operator interrupts
270+ to ask. ` src/subagent/fleet-report.ts ` closes that gap with its own channel:
271+ a system-notice line, pushed into the transcript through the same
272+ ` surfaceSystemNotice ` path as any other system row, the moment a lane
273+ transition is worth saying. It does not touch the panel's rows or its
274+ ` laneState() ` computation — it reads the same sub-agent session store the
275+ panel reads, and calls the same ` agentProgress() ` stall definition
276+ (` isStalled ` ) so the two surfaces never disagree about whether a lane is
277+ stalled, only about * when* they say so: the panel shows it continuously,
278+ the notice announces the transition once.
279+
280+ Store changes drive it directly, so a lane finishing or failing lands the
281+ moment it happens; a ` FLEET_REPORT_SETTLE_MS ` (400ms) timer covers the one
282+ change that produces no event at all — a lane going quiet — and also lets a
283+ parallel dispatch that lands as N store changes settle into one observation
284+ instead of N lines. Past ` COALESCE_ABOVE ` (3) changes in one observation the
285+ individual lines collapse into a single tally (` "9 done, 3 failed" ` ); below
286+ that threshold each change gets its own line. The one case both the fleet
287+ going idle and a coalesced tally would otherwise say the same thing —
288+ all changes are terminal and the tally alone already says "N done, N
289+ failed" — the idle line replaces the tally instead of repeating it with
290+ "— nothing running" tacked on.
291+
292+ Outcomes and errors are clipped to ` OUTCOME_CHARS ` /` MAX_UPDATE_CHARS ` on the
293+ same "one update is one row, never wrapped" rule the panel's rows follow.
294+ ` fleetDigest() ` is the on-demand counterpart: the same picture in one line,
295+ answering "where is the fleet" without an interrupt, for ` /fleet ` or an
296+ operator question mid-run.
297+
266298## How pop-ups should feel
267299
268300A blocking surface (permissions, an operator question, the model/provider
@@ -446,7 +478,11 @@ Ctrl+C interrupts a busy run (or clears a non-empty idle prompt); a second
446478Ctrl+C within a 2-second window (` CTRL_C_EXIT_WINDOW_MS ` ) quits — this
447479replaced an Ink-era yes/no exit-confirm modal with the same intent (an
448480explicit second confirmation) without adding a modal (` handleCtrlC ` ,
449- ` shell.ts ` ).
481+ ` shell.ts ` ). The interrupt keeps whatever is sitting in the queue rather than
482+ discarding it — the operator typed those messages meaning them delivered, not
483+ meaning "cancel this run and also throw away what I typed"; the transcript
484+ row says so (` "interrupt — N pending kept" ` ) and the kept items drain into the
485+ rebuilt agent at the next boundary (` applyShellInterrupt ` , ` shell.ts ` ).
450486
451487## Overflows, scrolling, and key macros
452488
0 commit comments