@@ -8,6 +8,7 @@ import { stripTerminalControlSequences } from "../../util/control-char-strip.js"
88import { isShellCommentOnly } from "../../permission/command.js" ;
99import { groupChainSegmentsForDisplay , middleEllipsis , verbatimCommandLines } from "../command-display.js" ;
1010import type { VerbatimLine } from "../command-display.js" ;
11+ import type { QueuedApprovalSummary } from "../hooks/use-gates.js" ;
1112
1213// Bidi controls (RLO, embeddings, isolates) visually reorder the rendered
1314// command — Trojan Source — and zero-width characters hide payload boundaries,
@@ -68,10 +69,24 @@ function truncateChoiceText(text: string, width: number): string {
6869 return middleEllipsis ( text , budget ) ;
6970}
7071
72+ // Deterministic color per agent label so queued approvals from different
73+ // sub-agents read as visually distinct without a shared color registry.
74+ const AGENT_TAG_ROLES = [ "accent" , "success" , "warning" , "syntaxKeyword" , "syntaxFunction" , "syntaxType" ] as const ;
75+
76+ function agentTagColor ( label : string ) : string {
77+ let hash = 0 ;
78+ for ( let i = 0 ; i < label . length ; i ++ ) hash = ( hash * 31 + label . charCodeAt ( i ) ) >>> 0 ;
79+ return color ( AGENT_TAG_ROLES [ hash % AGENT_TAG_ROLES . length ] ! ) ;
80+ }
81+
82+ const MAX_RENDERED_QUEUE_ENTRIES = 5 ;
83+
7184export type PermissionModalProps = {
7285 request : PermissionRequest ;
7386 /** Permission gates still queued, including this modal. */
7487 permissionQueueDepth ?: number ;
88+ /** Summary of every queued permission request, for the "queued behind" list. */
89+ queuedApprovals ?: readonly QueuedApprovalSummary [ ] ;
7590 /**
7691 * When set (goal mode), show that the request auto-skips after this many ms
7792 * if the operator does not answer.
@@ -187,11 +202,16 @@ function descriptorArgs(request: PermissionRequest): Record<string, unknown> {
187202export function PermissionModal ( {
188203 request,
189204 permissionQueueDepth = 1 ,
205+ queuedApprovals = [ ] ,
190206 goalTimeoutMs = null ,
191207 onResolve,
192208 width = 80 ,
193209} : PermissionModalProps ) : ReactNode {
194210 const queuedBehind = Math . max ( 0 , permissionQueueDepth - 1 ) ;
211+ // Everything behind the currently visible entry, distinguished by agent.
212+ const otherQueued = queuedApprovals . slice ( 1 ) ;
213+ const shownQueued = otherQueued . slice ( 0 , MAX_RENDERED_QUEUE_ENTRIES ) ;
214+ const hiddenQueuedCount = otherQueued . length - shownQueued . length ;
195215 const choices = buildChoices ( request ) ;
196216 const [ selected , setSelected ] = useState ( 0 ) ;
197217 const [ message , setMessage ] = useState ( "" ) ;
@@ -286,6 +306,14 @@ export function PermissionModal({
286306 width = { Math . max ( 24 , width - 2 ) }
287307 >
288308 < Text bold color = { toolColor } > Approval needed</ Text >
309+ { request . agentLabel !== undefined && (
310+ < Text >
311+ < Text color = { agentTagColor ( request . agentLabel ) } bold > { `⏺ ${ request . agentLabel } ` } </ Text >
312+ { request . cwd !== undefined && (
313+ < Text color = { color ( "muted" ) } > { ` ${ request . cwd } ` } </ Text >
314+ ) }
315+ </ Text >
316+ ) }
289317 { goalTimeoutSecs !== null && (
290318 < Text color = { color ( "muted" ) } >
291319 { `Goal mode · auto-skip in ~${ goalTimeoutSecs } s if no response` }
@@ -305,6 +333,24 @@ export function PermissionModal({
305333 ? ` · +${ queuedBehind } more approval${ queuedBehind === 1 ? "" : "s" } queued`
306334 : "" }
307335 </ Text >
336+ { shownQueued . length > 0 && (
337+ < Box marginLeft = { 2 } flexDirection = "column" >
338+ { shownQueued . map ( ( entry ) => (
339+ < Text key = { entry . id } color = { color ( "muted" ) } >
340+ { "· " }
341+ { entry . agentLabel !== undefined ? (
342+ < Text color = { agentTagColor ( entry . agentLabel ) } > { entry . agentLabel } </ Text >
343+ ) : (
344+ < Text color = { color ( "muted" ) } > session</ Text >
345+ ) }
346+ { ` — ${ entry . tool } ` }
347+ </ Text >
348+ ) ) }
349+ { hiddenQueuedCount > 0 && (
350+ < Text color = { color ( "muted" ) } > { `… ${ hiddenQueuedCount } more waiting` } </ Text >
351+ ) }
352+ </ Box >
353+ ) }
308354 { descriptor . isShell && (
309355 // The exact string that will execute, always shown verbatim: the
310356 // segment list below is a lossy reconstruction, and the scope hints
0 commit comments