@@ -11,6 +11,8 @@ import { shimmerText } from '#/tui/utils/shimmer';
1111const RESUMED_ITEM_LABEL = '(resumed)' ;
1212/** Divider between the cells that share a member row's free space. */
1313const MEMBER_SEPARATOR = ' · ' ;
14+ /** Marks a task cell whose shared preamble was dropped. One column wide. */
15+ const TASK_ELISION_MARK = '…' ;
1416const ORCHESTRATING_LABEL = 'Orchestrating' ;
1517const FINALIZING_LABEL = 'Finalizing' ;
1618// Pad to the wider live label so the suffix column never shifts between them.
@@ -440,8 +442,11 @@ export class DynamicWorkflowMissionControlComponent implements Component {
440442 const needsMore = members . length > slots ;
441443 const memberSlots = needsMore && slots >= 2 ? slots - 1 : slots ;
442444 const visibleMembers = members . slice ( 0 , Math . max ( 0 , memberSlots ) ) ;
445+ // Measured across every member, not the visible ones: a prefix that came
446+ // and went as rows scrolled would rewrite the task column under the eye.
447+ const sharedPrefix = sharedTaskPrefix ( members ) ;
443448 for ( const member of visibleMembers ) {
444- lines . push ( this . renderMember ( member , width , nowMs ) ) ;
449+ lines . push ( this . renderMember ( member , width , nowMs , sharedPrefix ) ) ;
445450 }
446451 const hidden = members . length - visibleMembers . length ;
447452 if ( hidden > 0 && lines . length < rowBudget ) {
@@ -567,7 +572,12 @@ export class DynamicWorkflowMissionControlComponent implements Component {
567572 return truncateToWidth ( currentTheme . fg ( 'textDim' , header ) , width ) ;
568573 }
569574
570- private renderMember ( member : DynamicWorkflowMember , width : number , nowMs : number ) : string {
575+ private renderMember (
576+ member : DynamicWorkflowMember ,
577+ width : number ,
578+ nowMs : number ,
579+ sharedPrefix : string ,
580+ ) : string {
571581 const id = currentTheme . fg ( 'primary' , String ( member . index ) . padStart ( 3 , '0' ) ) ;
572582 // All running rows share the workflow's clock, so they spin in step instead
573583 // of drifting apart by whenever each agent happened to start.
@@ -585,6 +595,11 @@ export class DynamicWorkflowMissionControlComponent implements Component {
585595 ? `${ id } ${ workColumn } ${ stateColumn } `
586596 : `${ id } ${ padToWidth ( state , 6 ) } ` ;
587597 const task = member . item || 'Delegated agent' ;
598+ // The elision is display-only: the dedup below still compares whole items,
599+ // so a streamed line that merely repeats the task is still suppressed.
600+ const shownTask = sharedPrefix . length > 0 && member . item . startsWith ( sharedPrefix )
601+ ? `${ TASK_ELISION_MARK } ${ member . item . slice ( sharedPrefix . length ) } `
602+ : task ;
588603 const latest = member . latest . length > 0 && member . latest !== task ? member . latest : undefined ;
589604 const detail = member . phase === 'suspended' || isTerminalPhase ( member . phase )
590605 ? member . statusDetail ?? latest
@@ -613,15 +628,15 @@ export class DynamicWorkflowMissionControlComponent implements Component {
613628 Math . floor ( rest * DYNAMIC_WORKFLOW_RENDERING . memberTaskShare ) ,
614629 ) ;
615630 const detailBudget = showWork && detail !== undefined && detail . length > 0
616- ? rest - Math . min ( visibleWidth ( task ) , taskCap ) - MEMBER_SEPARATOR . length
631+ ? rest - Math . min ( visibleWidth ( shownTask ) , taskCap ) - MEMBER_SEPARATOR . length
617632 : 0 ;
618633 const detailPart = detailBudget >= DYNAMIC_WORKFLOW_RENDERING . memberDetailMinWidth
619634 ? `${ MEMBER_SEPARATOR } ${ truncateToWidth ( currentTheme . fg ( 'textDim' , detail ?? '' ) , detailBudget ) } `
620635 : '' ;
621636
622637 // Whatever the detail did not take goes back to the task.
623638 const taskText = truncateToWidth (
624- currentTheme . fg ( 'text' , task ) ,
639+ currentTheme . fg ( 'text' , shownTask ) ,
625640 Math . max ( 1 , rest - visibleWidth ( detailPart ) ) ,
626641 ) ;
627642 return truncateToWidth (
@@ -1071,6 +1086,52 @@ function normalizeText(text: string | undefined): string {
10711086 return text ?. replaceAll ( / \s + / g, ' ' ) . trim ( ) ?? '' ;
10721087}
10731088
1089+ /**
1090+ * The preamble every task repeats, or `''` when dropping it would not help.
1091+ *
1092+ * `prompt_template` is optional, so a caller may pass a whole prompt as each
1093+ * item. Every row then opens with the same paragraph and the TASK column clips
1094+ * inside it — six rows reading `You are auditing the pythinker-code mono...`
1095+ * name nothing. Dropping the shared head once puts the tail that identifies the
1096+ * row back on screen.
1097+ *
1098+ * All-or-nothing on purpose: eliding a prefix that only some rows carry would
1099+ * make two cells at the same column mean different things.
1100+ */
1101+ function sharedTaskPrefix ( members : readonly DynamicWorkflowMember [ ] ) : string {
1102+ const items = members . map ( ( member ) => member . item ) . filter ( ( item ) => item . length > 0 ) ;
1103+ const first = items [ 0 ] ;
1104+ if ( first === undefined || items . length < 2 ) return '' ;
1105+
1106+ // Skips `first` against itself: that comparison can only return its own
1107+ // length, and it walks the whole string to say so on every animation frame.
1108+ let length = first . length ;
1109+ for ( const item of items . slice ( 1 ) ) {
1110+ length = commonPrefixLength ( first , item , length ) ;
1111+ if ( length === 0 ) return '' ;
1112+ }
1113+
1114+ // Cut at the last space inside the shared text. A cut mid-word reads as
1115+ // corruption, and a space is always a whole code unit, so ending there is
1116+ // also what keeps the slice off the middle of a surrogate pair.
1117+ //
1118+ // Backing off to before the last shared word is what leaves every row
1119+ // something after the mark: items are normalized, so none of them ends in a
1120+ // space, and the shortest one therefore still holds the word the cut skipped.
1121+ const boundary = first . lastIndexOf ( ' ' , length - 1 ) ;
1122+ if ( boundary < 0 ) return '' ;
1123+ const prefix = first . slice ( 0 , boundary + 1 ) ;
1124+ if ( visibleWidth ( prefix ) < DYNAMIC_WORKFLOW_RENDERING . memberTaskSharedPrefixMinWidth ) return '' ;
1125+ return prefix ;
1126+ }
1127+
1128+ function commonPrefixLength ( left : string , right : string , limit : number ) : number {
1129+ const bound = Math . min ( limit , left . length , right . length ) ;
1130+ let index = 0 ;
1131+ while ( index < bound && left [ index ] === right [ index ] ) index += 1 ;
1132+ return index ;
1133+ }
1134+
10741135/**
10751136 * The WORK cell: tool calls done, and how long this agent has been silent.
10761137 *
0 commit comments