@@ -88,6 +88,14 @@ import {
8888 overlayChromeRows ,
8989} from "../overlay-view.js" ;
9090import { DECISION_CHOICE_ROWS } from "../overlay-body.js" ;
91+ import type { PendingAskWake } from "../../subagent/fleet-report.js" ;
92+ import {
93+ composeWorkerWaitLine ,
94+ NO_WORKER_WAIT ,
95+ reduceWorkerWait ,
96+ type WorkerWaitPart ,
97+ type WorkerWaitRole ,
98+ } from "../worker-wait.js" ;
9199
92100import {
93101 type AppShell ,
@@ -165,6 +173,54 @@ export function setPluginNeedsAttention(shell: AppShell, needs: boolean): void {
165173 paintChrome ( shell ) ;
166174}
167175
176+ /**
177+ * Fold the latest pending-ask snapshot into the WORKER WAITING strip.
178+ *
179+ * The snapshot is authoritative: an identity leaves only when a later
180+ * snapshot omits it, never because its wake reached the director, a turn
181+ * settled, or the operator typed. Repaints only when the view model moved.
182+ */
183+ export function setWorkerWaitAsks (
184+ shell : AppShell ,
185+ asks : readonly PendingAskWake [ ] ,
186+ ) : void {
187+ const next = reduceWorkerWait ( shell . workerWait , asks ) ;
188+ if ( next === shell . workerWait ) return ;
189+ shell . workerWait = next ;
190+ paintChrome ( shell ) ;
191+ }
192+
193+ /** Teardown: drop every wait so the prompt box returns to normal chrome. */
194+ export function clearWorkerWait ( shell : AppShell ) : void {
195+ if ( shell . workerWait === NO_WORKER_WAIT ) return ;
196+ shell . workerWait = NO_WORKER_WAIT ;
197+ paintChrome ( shell ) ;
198+ }
199+
200+ /**
201+ * Wording carries the state (mark, label, routing copy); colour only lifts
202+ * the label to the action emphasis the shell already uses for "act here".
203+ */
204+ function workerWaitFg ( role : WorkerWaitRole ) : string {
205+ switch ( role ) {
206+ case "mark" :
207+ case "label" :
208+ return UI . action ;
209+ case "routing" :
210+ case "question" :
211+ return UI . text ;
212+ case "requester" :
213+ case "more" :
214+ return UI . textDim ;
215+ case "separator" :
216+ return UI . textFaint ;
217+ }
218+ }
219+
220+ function workerWaitChunks ( parts : readonly WorkerWaitPart [ ] ) : TextChunk [ ] {
221+ return parts . map ( ( part ) => fgChunk ( workerWaitFg ( part . role ) ) ( part . text ) ) ;
222+ }
223+
168224/**
169225 * Every input the chrome compose paths read, as one comparable key. A missed
170226 * input here means stale chrome, so this list is exhaustive:
@@ -182,13 +238,20 @@ export function setPluginNeedsAttention(shell: AppShell, needs: boolean): void {
182238 * `lockupChangedMs`, `lockupRampPhase`, `lockupStalledForMs`
183239 * - cost meter: band, percent label, cost label (or absence)
184240 * - landing suggestions: whether the prompt has text
241+ * - WORKER WAITING strip: its composed text at the current width (folds in
242+ * the selected identity, its preview, and the additional count)
185243 *
186244 * Landing and zone paints read their own state and do not pass through here.
187245 */
188- function chromeComposeKey ( shell : AppShell , notice : string ) : string {
246+ function chromeComposeKey (
247+ shell : AppShell ,
248+ notice : string ,
249+ workerWait : string ,
250+ ) : string {
189251 const meter = shell . costContext ;
190252 return [
191253 notice ,
254+ workerWait ,
192255 pendingColumnRows ( shell . session . items )
193256 . map ( ( row ) => `${ row . tag ?? "" } :${ row . text } ` )
194257 . join ( "\u0001" ) ,
@@ -240,7 +303,15 @@ export function paintChrome(
240303 if ( shell . renderer . isDestroyed || shell . notice . isDestroyed ) return ;
241304 syncPending ( shell ) ;
242305 const notice = noticeText ( shell ) ;
243- const key = chromeComposeKey ( shell , notice ) ;
306+ const workerWait = composeWorkerWaitLine (
307+ shell . workerWait ,
308+ shell . layout . contentWidth ,
309+ ) ;
310+ const key = chromeComposeKey (
311+ shell ,
312+ notice ,
313+ workerWait . map ( ( part ) => part . text ) . join ( "" ) ,
314+ ) ;
244315 if ( ! opts ?. force && paintedChromeKey . get ( shell ) === key ) return ;
245316 paintedChromeKey . set ( shell , key ) ;
246317 chromeComposeCounts . set ( shell , chromeComposeCount ( shell ) + 1 ) ;
@@ -252,6 +323,20 @@ export function paintChrome(
252323 syncLandingSuggestions ( shell ) ;
253324 syncNoticeRow ( shell , notice ) ;
254325 syncPendingColumn ( shell ) ;
326+ shell . workerWaitRow . content = new StyledText ( workerWaitChunks ( workerWait ) ) ;
327+ syncWorkerWaitRow ( shell , workerWait . length > 0 ) ;
328+ }
329+
330+ /**
331+ * Give the WORKER WAITING strip its row while any worker is parked, and take
332+ * it back once the final identity leaves the snapshot. Same transient
333+ * contract as the notice row; the resolver seats it, so it never overlaps.
334+ */
335+ function syncWorkerWaitRow ( shell : AppShell , wanted : boolean ) : void {
336+ const bag = shellInternals ( shell ) ;
337+ if ( bag === undefined ) return ;
338+ if ( ( bag . visibility . workerWait ?? false ) === wanted ) return ;
339+ relayout ( shell , { visibility : { ...bag . visibility , workerWait : wanted } } ) ;
255340}
256341
257342/**
@@ -879,6 +964,10 @@ export function applyLayout(shell: AppShell, layout: GeometryLayout): void {
879964 shell . pendingBox . height = pendingH > 0 ? pendingH : 1 ;
880965 shell . pendingBox . visible = pendingH > 0 ;
881966
967+ const workerWaitH = Math . max ( 0 , h . worker_wait ) ;
968+ shell . workerWaitRow . height = workerWaitH > 0 ? workerWaitH : 1 ;
969+ shell . workerWaitRow . visible = workerWaitH > 0 ;
970+
882971 const promptH = Math . max ( 0 , h . prompt ) ;
883972 shell . promptBox . height = promptH > 0 ? promptH : 1 ;
884973 shell . promptBox . visible = promptH > 0 ;
@@ -901,8 +990,9 @@ export function applyLayout(shell: AppShell, layout: GeometryLayout): void {
901990 // Rows the flow spends before the prompt box — where a floated host's bottom
902991 // edge has to land, since the landing's box sits mid-screen rather than at
903992 // the foot and covering it would hide the thing the operator types into.
904- // Stack: topPad, transcript, agents, task, then prompt (notice and the
905- // pending column omitted — both are transient chrome above the prompt).
993+ // Stack: topPad, transcript, agents, task, then prompt (notice, the
994+ // pending column and the worker-wait strip omitted: all transient chrome
995+ // above the prompt, so the float ends above them rather than over them).
906996 const promptTop = padH + transcriptBody + agentsH + taskH ;
907997 const hostH = floating
908998 ? Math . min ( overlayH , Math . max ( 1 , promptTop ) )
0 commit comments