Skip to content

Commit 03c5ec2

Browse files
committed
Document Ctrl+C's kept-queue behavior and the unprompted fleet channel
docs/TUI.md described Ctrl+C as discarding the queue, which stopped being true once the interrupt started keeping pending input. It also had no mention of the fleet-report notice channel added alongside the agents panel, so the two surfaces read as unrelated when they draw from the same store and stall definition. Also suppress the fleet's coalesced tally line when the idle line already says the same thing verbatim.
1 parent cba5c44 commit 03c5ec2

3 files changed

Lines changed: 48 additions & 7 deletions

File tree

docs/TUI.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
264264
same 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

268300
A 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
446478
Ctrl+C within a 2-second window (`CTRL_C_EXIT_WINDOW_MS`) quits — this
447479
replaced an Ink-era yes/no exit-confirm modal with the same intent (an
448480
explicit 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

src/subagent/fleet-report.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,7 @@ describe("observeFleet", () => {
131131
: { ...l, status: "failed" as const, error: "boom" },
132132
);
133133
const { updates } = observeFleet(seeded, after, T0 + 1000);
134-
expect(updates).toEqual([
135-
"fleet · 9 done, 3 failed",
136-
"fleet · 9 done, 3 failed — nothing running",
137-
]);
134+
expect(updates).toEqual(["fleet · 9 done, 3 failed — nothing running"]);
138135
});
139136
});
140137

src/subagent/fleet-report.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,17 @@ export function observeFleet(
186186
);
187187

188188
// The defect this report exists for: work finished, nothing left running,
189-
// and no one said so. That transition is always worth its own line.
189+
// and no one said so. That transition is always worth its own line — unless
190+
// the tally above already said the same thing, in which case a second line
191+
// restating it verbatim (with "— nothing running" tacked on) is noise, not
192+
// information.
190193
if (running === 0 && previous.running > 0) {
191-
lines.push(`${idleSummary(lanes)} — nothing running`);
194+
const idle = `${idleSummary(lanes)} — nothing running`;
195+
if (lines.length === 1 && lines[0] === idleSummary(lanes)) {
196+
lines[0] = idle;
197+
} else {
198+
lines.push(idle);
199+
}
192200
}
193201

194202
return {

0 commit comments

Comments
 (0)