@@ -59,6 +59,8 @@ const GOAL_TIMER_INTERVAL_MS = 1_000;
5959const TIP_ROTATE_INTERVAL_MS = 10_000 ;
6060const TIP_SEPARATOR = ' | ' ;
6161
62+ export type ToolOutputExpandHint = 'expand' | 'collapse' ;
63+
6264/**
6365 * Expand tips into a rotation sequence using smooth weighted round-robin
6466 * (the nginx SWRR algorithm). Higher-`priority` tips appear more often while
@@ -222,6 +224,7 @@ export class FooterComponent implements Component {
222224 */
223225 private backgroundBashTaskCount = 0 ;
224226 private backgroundAgentCount = 0 ;
227+ private expandHintProvider : ( ( ) => ToolOutputExpandHint | null ) | null = null ;
225228
226229 constructor ( state : AppState , onRefresh : ( ) => void = ( ) => { } ) {
227230 this . state = state ;
@@ -300,6 +303,10 @@ export class FooterComponent implements Component {
300303 * count produces its own bracketed badge on line 1; zeros hide them
301304 * independently.
302305 */
306+ setExpandHintProvider ( provider : ( ) => ToolOutputExpandHint | null ) : void {
307+ this . expandHintProvider = provider ;
308+ }
309+
303310 setBackgroundCounts ( counts : { bashTasks : number ; agentTasks : number } ) : void {
304311 this . backgroundBashTaskCount = Math . max ( 0 , counts . bashTasks ) ;
305312 this . backgroundAgentCount = Math . max ( 0 , counts . agentTasks ) ;
@@ -345,26 +352,20 @@ export class FooterComponent implements Component {
345352
346353 const leftWidth = visibleWidth ( leftLine ) ;
347354
348- // Rotating hint tips stay on the right unless they were given an
349- // inline slot in items (rendered above at their configured position)
350- // or the user dropped 'tips' from items.
351- let tipText = '' ;
352355 const tipsInline = order . includes ( 'tips' ) ;
353356 const showTips = ! tipsInline && ( configured === null || configured . includes ( 'tips' ) ) ;
357+ const tipCandidates : string [ ] = [ ] ;
354358 if ( showTips ) {
355359 const { primary, pair } = tipsForIndex ( currentTipIndex ( ) ) ;
356- const gap = 2 ;
357- const remaining = Math . max ( 0 , width - leftWidth - gap ) ;
358- if ( pair && visibleWidth ( pair ) <= remaining ) {
359- tipText = pair ;
360- } else if ( primary && visibleWidth ( primary ) <= remaining ) {
361- tipText = primary ;
362- }
360+ if ( pair ) tipCandidates . push ( pair ) ;
361+ if ( primary ) tipCandidates . push ( primary ) ;
363362 }
363+ const remaining = Math . max ( 0 , width - leftWidth - 2 ) ;
364+ const rightText = this . buildRightText ( tipCandidates , remaining , colors ) ;
364365
365- if ( tipText ) {
366- const pad = width - leftWidth - visibleWidth ( tipText ) ;
367- line1 = leftLine + ' ' . repeat ( Math . max ( 0 , pad ) ) + chalk . hex ( colors . textMuted ) ( tipText ) ;
366+ if ( rightText . length > 0 ) {
367+ const pad = width - leftWidth - visibleWidth ( rightText ) ;
368+ line1 = leftLine + ' ' . repeat ( Math . max ( 0 , pad ) ) + rightText ;
368369 } else if ( leftWidth <= width ) {
369370 line1 = leftLine ;
370371 } else {
@@ -395,8 +396,14 @@ export class FooterComponent implements Component {
395396 chalk . hex ( colors . text ) ( contextText ) +
396397 chalk . hex ( colors . textDim ) ( speedSuffix ) ;
397398 } else {
398- const leftPad = Math . max ( 0 , width - rightWidth ) ;
399+ const shortcut = customLine !== null ? this . expandShortcut ( ) : null ;
400+ const left =
401+ shortcut !== null && visibleWidth ( shortcut ) + 1 + rightWidth <= width
402+ ? chalk . hex ( colors . textDim ) ( shortcut )
403+ : '' ;
404+ const leftPad = Math . max ( 0 , width - visibleWidth ( left ) - rightWidth ) ;
399405 line2 =
406+ left +
400407 ' ' . repeat ( leftPad ) +
401408 chalk . hex ( colors . text ) ( contextText ) +
402409 chalk . hex ( colors . textDim ) ( speedSuffix ) ;
@@ -405,6 +412,28 @@ export class FooterComponent implements Component {
405412 return [ truncateToWidth ( line1 , width ) , truncateToWidth ( line2 , width ) ] ;
406413 }
407414
415+ private expandShortcut ( ) : string | null {
416+ const hint = this . expandHintProvider ?.( ) ?? null ;
417+ return hint === null ? null : `ctrl+o ${ hint } ` ;
418+ }
419+
420+ private buildRightText ( tips : readonly string [ ] , remaining : number , colors : ColorPalette ) : string {
421+ const shortcut = this . expandShortcut ( ) ;
422+ if ( shortcut === null ) {
423+ const tip = tips . find ( ( candidate ) => visibleWidth ( candidate ) <= remaining ) ;
424+ return tip === undefined ? '' : chalk . hex ( colors . textMuted ) ( tip ) ;
425+ }
426+ for ( const tip of tips ) {
427+ if ( visibleWidth ( `${ shortcut } ${ TIP_SEPARATOR } ${ tip } ` ) <= remaining ) {
428+ return (
429+ chalk . hex ( colors . textDim ) ( shortcut ) +
430+ chalk . hex ( colors . textMuted ) ( `${ TIP_SEPARATOR } ${ tip } ` )
431+ ) ;
432+ }
433+ }
434+ return visibleWidth ( shortcut ) <= remaining ? chalk . hex ( colors . textDim ) ( shortcut ) : '' ;
435+ }
436+
408437 /**
409438 * Rendered pieces per status-line slot. Empty-content slots (e.g. no goal,
410439 * outside a git repo) yield an empty list so composition just skips them.
0 commit comments