Skip to content

Commit 108bae6

Browse files
committed
feat(tui): show parked worker questions on the prompt box
1 parent 0863141 commit 108bae6

11 files changed

Lines changed: 768 additions & 7 deletions

File tree

‎docs/TUI.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,30 @@ poll uses
307307
needed it **does not** call `bridge.syncAgentProgress` — chrome owns the live
308308
clocks.
309309

310+
### WORKER WAITING strip
311+
312+
While any root worker is parked on `ask_director`, a one-row **WORKER
313+
WAITING** strip sits directly on the prompt box (zone `worker_wait`):
314+
315+
```
316+
◆ WORKER WAITING · director reply needed (send_input) · builder (copy assets): Which destination path should I use? (+1 more)
317+
```
318+
319+
It names the worker, previews its question, and says the director owns the
320+
reply. It is display only: composer text still goes to the primary director,
321+
and only the director's `send_input` to the worker's session answers the
322+
worker. The strip reads the `agent-ask` pending snapshot and nothing else
323+
(`src/tui/worker-wait.ts`, painted by `paintChrome` in `shell/chrome.ts`). An
324+
item's identity is `sessionId + questionId`, and it leaves only when a later
325+
snapshot omits that identity. Delivering the wake, director inference,
326+
typing, submitting, scrolling, and redraws do not clear it. Session rotation,
327+
stop teardown, and bridge dispose clear it. One question shows at a time,
328+
with `(+N more)` for the rest; the displayed identity stays while it is live,
329+
and otherwise the first snapshot entry shows. At narrow widths the question
330+
truncates first, then requester metadata and routing copy shorten, while the
331+
waiting label and the count stay. Primary-session `ask_operator` prompts keep
332+
their own overlay and never use this strip.
333+
310334
### Transcript spawn_agent rows (history anchors)
311335

312336
`runtime-bridge` paints each `spawn_agent` call as a transcript stream row for

‎src/tui/chrome-repaint.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { describe, expect, test } from "bun:test";
66
import { withTestRenderer } from "./harness";
77
import {
88
chromeComposeCount,
9+
clearWorkerWait,
910
paintChrome,
1011
setLockupFrame,
1112
setStatusFlash,
13+
setWorkerWaitAsks,
1214
} from "./shell/chrome";
1315
import { createAppShell } from "./shell/index";
1416
import type { AppShell } from "./shell/internals";
@@ -112,4 +114,43 @@ describe("chrome repaint gate", () => {
112114
expect(chromeComposeCount(shell)).toBe(baseline + 2);
113115
});
114116
});
117+
118+
test("worker wait snapshots recompose on change and never on a repeat", async () => {
119+
await withShell((shell) => {
120+
const ask = {
121+
sessionId: "sess-1",
122+
agentId: "builder",
123+
description: "copy assets",
124+
question: "Which destination path should I use?",
125+
questionId: "q1",
126+
};
127+
paintChrome(shell);
128+
const baseline = chromeComposeCount(shell);
129+
130+
setWorkerWaitAsks(shell, [ask]);
131+
const shown = chromeComposeCount(shell);
132+
expect(shown).toBeGreaterThan(baseline);
133+
expect(shell.workerWaitRow.visible).toBe(true);
134+
135+
// Repeat reports of the same identity and idle ticks cost nothing.
136+
setWorkerWaitAsks(shell, [ask]);
137+
setWorkerWaitAsks(shell, [{ ...ask }]);
138+
for (let tick = 0; tick < 5; tick++) paintChrome(shell);
139+
expect(chromeComposeCount(shell)).toBe(shown);
140+
141+
// A replacement question changes the composed strip: one recompose.
142+
setWorkerWaitAsks(shell, [
143+
{ ...ask, questionId: "q2", question: "Overwrite?" },
144+
]);
145+
expect(chromeComposeCount(shell)).toBe(shown + 1);
146+
147+
// The final removal recomposes and hands the row back.
148+
setWorkerWaitAsks(shell, []);
149+
expect(chromeComposeCount(shell)).toBeGreaterThan(shown + 1);
150+
expect(shell.workerWaitRow.visible).toBe(false);
151+
const cleared = chromeComposeCount(shell);
152+
clearWorkerWait(shell);
153+
expect(chromeComposeCount(shell)).toBe(cleared);
154+
});
155+
});
115156
});

‎src/tui/geometry.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ describe("zone registry", () => {
3434
});
3535
});
3636

37+
describe("worker wait zone", () => {
38+
test("seats one row directly on the prompt box without shrinking it", () => {
39+
const idle = idle80x24();
40+
const layout = idle80x24({ visibility: { workerWait: true } });
41+
const strip = defined(layout.regions.worker_wait, "worker_wait");
42+
const prompt = defined(layout.regions.prompt, "prompt");
43+
expect(strip.height).toBe(1);
44+
expect(strip.y + strip.height).toBe(prompt.y);
45+
expect(layout.heights.prompt).toBe(idle.heights.prompt);
46+
expect(layout.transcriptHeight).toBe(idle.transcriptHeight - 1);
47+
});
48+
49+
test("is the last optional row cut before prompt growth is reclaimed", () => {
50+
expect(COLLAPSE_ORDER.indexOf("worker_wait")).toBe(
51+
COLLAPSE_ORDER.indexOf("prompt") - 1,
52+
);
53+
const layout = resolveGeometry({
54+
terminal: { columns: 80, rows: 18 },
55+
visibility: { workerWait: true, notice: true, pending: 2 },
56+
});
57+
expect(layout.heights.notice).toBe(0);
58+
expect(layout.heights.pending).toBe(0);
59+
expect(layout.heights.worker_wait).toBe(1);
60+
});
61+
});
62+
3763
describe("resolveGeometry — 80×24 idle floor", () => {
3864
test("idle default chrome yields transcript ≥ 12", () => {
3965
const layout = idle80x24();

‎src/tui/geometry/resolve.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface ZoneVisibility {
3939
readonly notice?: boolean;
4040
/** Pending queue column: exact row count requested (bounded by the zone max). */
4141
readonly pending?: boolean | number;
42+
/** WORKER WAITING strip on (default off). */
43+
readonly workerWait?: boolean;
4244
/** Progress: false/omit = 0; true = 2; or explicit 1|2. */
4345
readonly progress?: boolean | 1 | 2;
4446
/** Progress divider (0–1). Default on when progress is shown. */
@@ -147,6 +149,7 @@ export function desiredHeights(input: GeometryInput): MutableHeights {
147149
progress_divider: progressDivider,
148150
notice: vis.notice === true ? 1 : ZONE_REGISTRY.notice.idleDefault,
149151
pending: clamp(boolOrRows(vis.pending, 1), 0, ZONE_REGISTRY.pending.max),
152+
worker_wait: vis.workerWait === true ? 1 : 0,
150153
prompt: promptRows,
151154
task: clamp(boolOrRows(vis.task, 1), 0, ZONE_REGISTRY.task.max),
152155
// The board asks for exactly the rows it paints; the fraction stops a large

‎src/tui/geometry/zones.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const ZONE_IDS = [
88
"progress_divider",
99
"notice",
1010
"pending",
11+
"worker_wait",
1112
"prompt",
1213
"task",
1314
"agents",
@@ -82,6 +83,15 @@ export const ZONE_REGISTRY: Readonly<Record<ZoneId, ZoneDeclaration>> = {
8283
idleDefault: 0,
8384
alwaysOn: false,
8485
},
86+
// WORKER WAITING strip: one row on the prompt box while a root worker is
87+
// parked on ask_director. Off whenever nothing is waiting.
88+
worker_wait: {
89+
id: "worker_wait",
90+
min: 0,
91+
max: 1,
92+
idleDefault: 0,
93+
alwaysOn: false,
94+
},
8595
// Grows with the draft; the resolver caps it at PROMPT_CAP_FRACTION and
8696
// collapses toward min when the transcript would breach its floor.
8797
prompt: {
@@ -191,9 +201,13 @@ export const COLLAPSE_ORDER = [
191201
"progress",
192202
"progress_divider",
193203
"notice",
194-
// Pending items are the operator's own queued words: cut last of the
195-
// optionals, just ahead of prompt growth reclaim.
204+
// Pending items are the operator's own queued words: cut late, after every
205+
// banner and strip.
196206
"pending",
207+
// A parked worker is standing state the operator cannot see anywhere else
208+
// once the wake turn scrolls away, so its single row is the last optional
209+
// cut, just ahead of prompt growth reclaim.
210+
"worker_wait",
197211
// prompt growth reclaimed next (handled specially; never below PROMPT_BASE_ROWS)
198212
"prompt",
199213
] as const satisfies readonly ZoneId[];
@@ -212,6 +226,7 @@ export const PAINT_ORDER = [
212226
"progress_divider",
213227
"notice",
214228
"pending",
229+
"worker_wait",
215230
"prompt",
216231
] as const satisfies readonly ZoneId[];
217232

0 commit comments

Comments
 (0)