55 * Uses the callback form of setWidget for themed rendering.
66 */
77
8+ import { stripVTControlCharacters } from "node:util" ;
89import { truncateToWidth } from "@earendil-works/pi-tui" ;
910import type { AgentManager } from "../agent-manager.js" ;
1011import { getConfig } from "../agent-types.js" ;
@@ -76,7 +77,7 @@ export interface AgentDetails {
7677 activity ?: string ;
7778 /** Current spinner frame index (for animated running indicator). */
7879 spinnerFrame ?: number ;
79- /** Short model name if different from parent (e.g. "haiku", "sonnet") . */
80+ /** Effective model display name used by this run . */
8081 modelName ?: string ;
8182 /** Notable config tags (e.g. ["thinking: high", "isolated"]). */
8283 tags ?: string [ ] ;
@@ -160,6 +161,12 @@ export function getPromptModeLabel(type: SubagentType): string | undefined {
160161 return config . promptMode === "append" ? "twin" : undefined ;
161162}
162163
164+ /** Make a model name safe for single-line terminal display. */
165+ export function prepareModelNameForDisplay ( modelName : string | undefined ) : string | undefined {
166+ if ( ! modelName ) return undefined ;
167+ return stripVTControlCharacters ( modelName ) . replace ( / [ \u0000 - \u001f \u007f - \u009f ] + / g, " " ) . trim ( ) ;
168+ }
169+
163170/** Mode label is not included — callers add it where they want it. */
164171export function buildInvocationTags (
165172 invocation : AgentInvocation | undefined ,
@@ -172,7 +179,7 @@ export function buildInvocationTags(
172179 if ( invocation . inheritContext ) tags . push ( "inherit context" ) ;
173180 if ( invocation . runInBackground ) tags . push ( "background" ) ;
174181 if ( invocation . maxTurns != null ) tags . push ( `max turns: ${ invocation . maxTurns } ` ) ;
175- return { modelName : invocation . modelName , tags } ;
182+ return { modelName : prepareModelNameForDisplay ( invocation . modelName ) , tags } ;
176183}
177184
178185/** Truncate text to a single line, max `len` chars. */
@@ -305,8 +312,43 @@ export class AgentWidget {
305312 }
306313 }
307314
315+ /** Model and thinking metadata paired for the current run. */
316+ private invocationStats (
317+ a : {
318+ invocation ?: AgentInvocation ;
319+ session ?: { model ?: { provider : string ; id : string } ; thinkingLevel ?: string } ;
320+ } ,
321+ theme : Theme ,
322+ ) : string | undefined {
323+ const runtimeInvocation = a . session ?. model
324+ ? {
325+ ...a . invocation ,
326+ modelName : `${ a . session . model . provider } /${ a . session . model . id } ` ,
327+ thinking : a . session . thinkingLevel ,
328+ }
329+ : a . invocation ;
330+ const { modelName, tags } = buildInvocationTags ( runtimeInvocation ) ;
331+ const safeModelName = prepareModelNameForDisplay ( modelName ) ;
332+ const parts = safeModelName ? [ safeModelName , ...tags . filter ( tag => tag . startsWith ( "thinking: " ) ) ] : [ ] ;
333+ return parts . length > 0 ? theme . fg ( "dim" , parts . join ( " · " ) ) : undefined ;
334+ }
335+
308336 /** Render a finished agent line. */
309- private renderFinishedLine ( a : { id : string ; type : SubagentType ; status : string ; description : string ; toolUses : number ; startedAt : number ; completedAt ?: number ; error ?: string } , theme : Theme ) : string {
337+ private renderFinishedLine (
338+ a : {
339+ id : string ;
340+ type : SubagentType ;
341+ status : string ;
342+ description : string ;
343+ toolUses : number ;
344+ startedAt : number ;
345+ completedAt ?: number ;
346+ error ?: string ;
347+ invocation ?: AgentInvocation ;
348+ session ?: { model ?: { provider : string ; id : string } ; thinkingLevel ?: string } ;
349+ } ,
350+ theme : Theme ,
351+ ) : string {
310352 const name = getDisplayName ( a . type ) ;
311353 const modeLabel = getPromptModeLabel ( a . type ) ;
312354 const duration = formatMs ( ( a . completedAt ?? Date . now ( ) ) - a . startedAt ) ;
@@ -333,6 +375,8 @@ export class AgentWidget {
333375 }
334376
335377 const parts : string [ ] = [ ] ;
378+ const invocationStats = this . invocationStats ( a , theme ) ;
379+ if ( invocationStats ) parts . push ( invocationStats ) ;
336380 const activity = this . agentActivity . get ( a . id ) ;
337381 if ( activity ) parts . push ( formatTurns ( activity . turnCount , activity . maxTurns ) ) ;
338382 if ( a . toolUses > 0 ) parts . push ( `${ a . toolUses } tool use${ a . toolUses === 1 ? "" : "s" } ` ) ;
@@ -389,6 +433,8 @@ export class AgentWidget {
389433 const tokenText = tokens > 0 ? formatSessionTokens ( tokens , contextPercent , theme , a . compactionCount ) : "" ;
390434
391435 const parts : string [ ] = [ ] ;
436+ const invocationStats = this . invocationStats ( a , theme ) ;
437+ if ( invocationStats ) parts . push ( invocationStats ) ;
392438 if ( bg ) parts . push ( formatTurns ( bg . turnCount , bg . maxTurns ) ) ;
393439 if ( toolUses > 0 ) parts . push ( `${ toolUses } tool use${ toolUses === 1 ? "" : "s" } ` ) ;
394440 if ( tokenText ) parts . push ( tokenText ) ;
@@ -403,29 +449,35 @@ export class AgentWidget {
403449 ] ) ;
404450 }
405451
406- const queuedLine = queued . length > 0
407- ? truncate ( theme . fg ( "dim" , "├─" ) + ` ${ theme . fg ( "muted" , "◦" ) } ${ theme . fg ( "dim" , `${ queued . length } queued` ) } ` )
408- : undefined ;
452+ const queuedLines = queued . map ( a => {
453+ const invocationStats = this . invocationStats ( a , theme ) ;
454+ const suffix = invocationStats ? ` · ${ invocationStats } ` : "" ;
455+ return truncate (
456+ theme . fg ( "dim" , "├─" ) +
457+ ` ${ theme . fg ( "muted" , "◦" ) } ${ theme . bold ( getDisplayName ( a . type ) ) } ${ theme . fg ( "muted" , a . description ) } ` +
458+ theme . fg ( "dim" , suffix ) ,
459+ ) ;
460+ } ) ;
409461
410462 // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
411463 const maxBody = MAX_WIDGET_LINES - 1 ; // heading takes 1 line
412- const totalBody = finishedLines . length + runningLines . length * 2 + ( queuedLine ? 1 : 0 ) ;
464+ const totalBody = finishedLines . length + runningLines . length * 2 + queuedLines . length ;
413465
414466 const lines : string [ ] = [ truncate ( theme . fg ( headingColor , headingIcon ) + " " + theme . fg ( headingColor , "Agents" ) ) ] ;
415467
416468 if ( totalBody <= maxBody ) {
417469 // Everything fits — add all lines and fix up connectors for the last item.
418470 lines . push ( ...finishedLines ) ;
419471 for ( const pair of runningLines ) lines . push ( ...pair ) ;
420- if ( queuedLine ) lines . push ( queuedLine ) ;
472+ lines . push ( ... queuedLines ) ;
421473
422474 // Fix last connector: swap ├─ → └─ and │ → space for activity lines.
423475 if ( lines . length > 1 ) {
424476 const last = lines . length - 1 ;
425477 lines [ last ] = lines [ last ] . replace ( "├─" , "└─" ) ;
426478 // If last item is a running agent activity line, fix indent of that line
427479 // and fix the header line above it.
428- if ( runningLines . length > 0 && ! queuedLine ) {
480+ if ( runningLines . length > 0 && queuedLines . length === 0 ) {
429481 // The last two lines are the last running agent's header + activity.
430482 if ( last >= 2 ) {
431483 lines [ last - 1 ] = lines [ last - 1 ] . replace ( "├─" , "└─" ) ;
@@ -438,6 +490,7 @@ export class AgentWidget {
438490 // Reserve 1 line for overflow indicator.
439491 let budget = maxBody - 1 ;
440492 let hiddenRunning = 0 ;
493+ let hiddenQueued = 0 ;
441494 let hiddenFinished = 0 ;
442495
443496 // 1. Running agents (2 lines each)
@@ -450,10 +503,14 @@ export class AgentWidget {
450503 }
451504 }
452505
453- // 2. Queued line
454- if ( queuedLine && budget >= 1 ) {
455- lines . push ( queuedLine ) ;
456- budget -- ;
506+ // 2. Queued agents
507+ for ( const line of queuedLines ) {
508+ if ( budget >= 1 ) {
509+ lines . push ( line ) ;
510+ budget -- ;
511+ } else {
512+ hiddenQueued ++ ;
513+ }
457514 }
458515
459516 // 3. Finished agents
@@ -469,9 +526,10 @@ export class AgentWidget {
469526 // Overflow summary
470527 const overflowParts : string [ ] = [ ] ;
471528 if ( hiddenRunning > 0 ) overflowParts . push ( `${ hiddenRunning } running` ) ;
529+ if ( hiddenQueued > 0 ) overflowParts . push ( `${ hiddenQueued } queued` ) ;
472530 if ( hiddenFinished > 0 ) overflowParts . push ( `${ hiddenFinished } finished` ) ;
473531 const overflowText = overflowParts . join ( ", " ) ;
474- lines . push ( truncate ( theme . fg ( "dim" , "└─" ) + ` ${ theme . fg ( "dim" , `+${ hiddenRunning + hiddenFinished } more (${ overflowText } )` ) } ` )
532+ lines . push ( truncate ( theme . fg ( "dim" , "└─" ) + ` ${ theme . fg ( "dim" , `+${ hiddenRunning + hiddenQueued + hiddenFinished } more (${ overflowText } )` ) } ` )
475533 ) ;
476534 }
477535
0 commit comments