Skip to content

Commit f513583

Browse files
committed
Keep fleet board disclosures honest under re-clamp
When geometry grants fewer rows than the formatter already folded, carry the prior +N more / header hidden count into the new total so operators still see every running lane accounted for. Paint lane state as operator copy (in tool) rather than the machine token.
1 parent 10a3d61 commit f513583

2 files changed

Lines changed: 97 additions & 9 deletions

File tree

src/tui-opentui/chrome-state.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
22
import {
33
annotateAgentTools,
44
chromeFromSession,
5+
clampBoardRows,
56
formatAgentsPanel,
67
formatChromeZones,
78
formatTasksPanel,
@@ -447,12 +448,14 @@ describe("lane state survives the mapping hops", () => {
447448
undefined,
448449
NOW,
449450
)
450-
// Board: header first, then the lane. Expect main's in_tool vocabulary.
451+
// Board: header first, then the lane. Operator copy uses "in tool", not
452+
// the machine LaneState token.
451453
expect(rows?.[0]?.kind).toBe("header")
452454
expect(rows?.[0]?.label).toContain("in tool")
453455
expect(rows?.[1]?.kind).toBe("lane")
454456
expect(rows?.[1]?.stalled).toBe(false)
455-
expect(rows?.[1]?.tail).toContain("in_tool")
457+
expect(rows?.[1]?.tail).toContain("in tool")
458+
expect(rows?.[1]?.tail).not.toContain("in_tool")
456459
expect(rows?.[1]?.tail).toContain("run_shell 1:30")
457460
expect(rows?.[1]?.tail).not.toContain("stalled")
458461

@@ -495,3 +498,42 @@ describe("lane state survives the mapping hops", () => {
495498
expect(annotated.agents?.[0]?.currentToolStartedAt).toBeNull()
496499
})
497500
})
501+
502+
describe("clampBoardRows", () => {
503+
test("carries a prior more-row count into a tighter re-clamp", () => {
504+
// Formatter already hid 4 of 8; collapse then grants only 4 rows total.
505+
// Honest disclosure is 4 prior + 2 newly dropped = 6, not 2.
506+
const formatted = [
507+
{ label: "FLEET 8 lanes · 8 working", tail: "", stalled: false, kind: "header" as const },
508+
{ label: "a: one", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
509+
{ label: "b: two", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
510+
{ label: "c: three", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
511+
{ label: "d: four", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
512+
{ label: "+4 more lanes", tail: "", stalled: false, kind: "more" as const },
513+
]
514+
const clamped = clampBoardRows(formatted, 4)
515+
expect(clamped).toHaveLength(4)
516+
expect(clamped[0]?.kind).toBe("header")
517+
expect(clamped[0]?.tail).toBe("")
518+
expect(clamped[3]).toEqual({
519+
label: "+6 more lanes",
520+
tail: "",
521+
stalled: false,
522+
kind: "more",
523+
})
524+
})
525+
526+
test("under a tight height the header carries the total hidden count", () => {
527+
const formatted = [
528+
{ label: "FLEET 8 lanes · 8 working", tail: " · +4 hidden", stalled: false, kind: "header" as const },
529+
{ label: "a: one", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
530+
{ label: "b: two", tail: " · working · 0:01", stalled: false, kind: "lane" as const },
531+
]
532+
const clamped = clampBoardRows(formatted, 2)
533+
expect(clamped).toHaveLength(2)
534+
// 4 prior + 1 newly dropped lane = 5.
535+
expect(clamped[0]?.tail).toBe(" · +5 hidden")
536+
expect(clamped.some((r) => r.kind === "more")).toBe(false)
537+
})
538+
})
539+

src/tui-opentui/chrome-state.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ function boardLaneState(
285285
* zone's box — rows land on top of each other and on whatever is below. So the
286286
* granted height is the last word, and the lanes it costs are disclosed rather
287287
* than dropped in silence.
288+
*
289+
* When the formatter already folded a fan-out (`+N more lanes` or header
290+
* `+N hidden`), that prior count is carried into the re-clamp total so the
291+
* operator still sees every running lane accounted for — not only the ones
292+
* still present as row objects after the first fold.
288293
*/
289294
export function clampBoardRows(
290295
rows: readonly AgentPanelRow[],
@@ -296,27 +301,68 @@ export function clampBoardRows(
296301
const header = rows[0]
297302
if (header === undefined) return []
298303
const lanes = rows.filter((r) => r.kind === "lane")
304+
const priorHidden = priorHiddenCount(rows)
305+
// Drop any prior disclosure on the header; we restate the total below.
306+
const cleanHeader = stripHiddenTail(header)
299307

300308
// Below a few rows the disclosure line costs more than the lane it displaces,
301309
// so the header carries the count instead — the same trade the formatter makes.
302310
if (height < 4) {
303-
const shown = lanes.slice(0, height - 1)
304-
return [withHiddenCount(header, lanes.length - shown.length), ...shown]
311+
const shown = lanes.slice(0, Math.max(0, height - 1))
312+
const hidden = priorHidden + (lanes.length - shown.length)
313+
return [withHiddenCount(cleanHeader, hidden), ...shown]
305314
}
306315

307-
const shown = lanes.slice(0, height - 2)
308-
const hidden = lanes.length - shown.length
316+
const shown = lanes.slice(0, Math.max(0, height - 2))
317+
const hidden = priorHidden + (lanes.length - shown.length)
309318
return [
310-
header,
319+
cleanHeader,
311320
...shown,
312321
{ label: `+${hidden} more lanes`, tail: "", stalled: false, kind: "more" },
313322
]
314323
}
315324

325+
/** Lanes already disclosed by a prior format/clamp fold on these rows. */
326+
function priorHiddenCount(rows: readonly AgentPanelRow[]): number {
327+
let hidden = 0
328+
for (const row of rows) {
329+
if (row.kind === "more") {
330+
const match = /^\+(\d+) more lanes$/.exec(row.label)
331+
if (match?.[1] !== undefined) hidden += Number(match[1])
332+
continue
333+
}
334+
if (row.kind === "header") {
335+
const match = / · \+(\d+) hidden$/.exec(row.tail)
336+
if (match?.[1] !== undefined) hidden += Number(match[1])
337+
}
338+
}
339+
return hidden
340+
}
341+
342+
function stripHiddenTail(header: AgentPanelRow): AgentPanelRow {
343+
const tail = header.tail.replace(/ · \+\d+ hidden$/, "")
344+
return tail === header.tail ? header : { ...header, tail }
345+
}
346+
316347
function withHiddenCount(header: AgentPanelRow, hidden: number): AgentPanelRow {
317348
return hidden > 0 ? { ...header, tail: ` · +${hidden} hidden` } : header
318349
}
319350

351+
/**
352+
* Operator-facing state word. Machine `LaneState` stays snake_case for code;
353+
* the board never paints that vocabulary into the terminal.
354+
*/
355+
function laneStateWord(state: LaneState): string {
356+
switch (state) {
357+
case "in_tool":
358+
return "in tool"
359+
case "stalled":
360+
return "stalled"
361+
case "working":
362+
return "working"
363+
}
364+
}
365+
320366
/**
321367
* The one-line answer to "is everything fine". Counts run worst-first so that
322368
* a narrow terminal ellipsizes away the routine tail rather than the trouble.
@@ -366,12 +412,12 @@ function formatAgentRow(
366412

367413
// Prefer main's agentProgress for tool-clock / in_tool / quiet clocks so the
368414
// board never invents a second stall path. Board presentation still prefixes
369-
// the state word (and kind: lane) the way the fleet board reads.
415+
// the operator-facing state word (and kind: lane) the way the fleet board reads.
370416
const progress = agentProgress(progressSession, nowMs, stallMs)
371417
if (progress !== null) {
372418
return {
373419
label,
374-
tail: ` · ${state} · ${progress.stat}`,
420+
tail: ` · ${laneStateWord(state)} · ${progress.stat}`,
375421
stalled,
376422
kind: "lane",
377423
}

0 commit comments

Comments
 (0)