@@ -11,20 +11,43 @@ import type { Component } from '@earendil-works/pi-tui';
1111import type { GoalChange } from '@moonshot-ai/kimi-code-sdk' ;
1212import chalk from 'chalk' ;
1313
14+ import { STATUS_BULLET } from '#/tui/constant/symbols' ;
1415import type { ColorPalette } from '#/tui/theme/colors' ;
1516
1617const HEAD_INDENT = ' ' ;
1718const DETAIL_INDENT = ' ' ;
1819
20+ type GoalMarkerActor = 'user' | 'model' | 'runtime' | 'system' ;
21+
22+ interface GoalMarkerOptions {
23+ readonly marker ?: string ;
24+ readonly textHex ?: string ;
25+ readonly expandable ?: boolean ;
26+ readonly indent ?: string ;
27+ readonly leadingBlank ?: boolean ;
28+ }
29+
1930export class GoalMarkerComponent implements Component {
2031 private expanded = false ;
32+ private readonly marker : string ;
33+ private readonly textHex : string ;
34+ private readonly expandable : boolean ;
35+ private readonly indent : string ;
36+ private readonly leadingBlank : boolean ;
2137
2238 constructor (
2339 private readonly headline : string ,
2440 private readonly detail : string | undefined ,
2541 private readonly colors : ColorPalette ,
2642 private readonly accentHex : string ,
27- ) { }
43+ options : GoalMarkerOptions = { } ,
44+ ) {
45+ this . marker = options . marker ?? '◦' ;
46+ this . textHex = options . textHex ?? colors . textDim ;
47+ this . expandable = options . expandable ?? true ;
48+ this . indent = options . indent ?? HEAD_INDENT ;
49+ this . leadingBlank = options . leadingBlank ?? false ;
50+ }
2851
2952 invalidate ( ) : void { }
3053
@@ -33,20 +56,29 @@ export class GoalMarkerComponent implements Component {
3356 }
3457
3558 render ( width : number ) : string [ ] {
36- const dot = chalk . hex ( this . accentHex ) ( '◦' ) ;
37- const head = chalk . hex ( this . colors . textDim ) ( this . headline ) ;
59+ const dot = chalk . hex ( this . accentHex ) ( this . marker ) ;
60+ const head = chalk . hex ( this . textHex ) ( this . headline ) ;
3861 const hasDetail = this . detail !== undefined && this . detail . length > 0 ;
39- if ( ! hasDetail ) return [ `${ HEAD_INDENT } ${ dot } ${ head } ` ] ;
62+ if ( ! hasDetail ) return this . withLeadingBlank ( [ `${ this . indent } ${ dot } ${ head } ` ] ) ;
4063
64+ if ( ! this . expandable ) {
65+ return this . withLeadingBlank ( [ `${ this . indent } ${ dot } ${ head } ` ] ) ;
66+ }
4167 if ( ! this . expanded ) {
42- return [ `${ HEAD_INDENT } ${ dot } ${ head } ${ chalk . hex ( this . colors . textMuted ) ( '(ctrl+o)' ) } ` ] ;
68+ return this . withLeadingBlank ( [
69+ `${ this . indent } ${ dot } ${ head } ${ chalk . hex ( this . colors . textMuted ) ( '(ctrl+o)' ) } ` ,
70+ ] ) ;
4371 }
44- const out = [ `${ HEAD_INDENT } ${ dot } ${ head } ` ] ;
72+ const out = [ `${ this . indent } ${ dot } ${ head } ` ] ;
4573 const wrapWidth = Math . max ( 20 , width - DETAIL_INDENT . length ) ;
4674 for ( const line of wrap ( this . detail ! , wrapWidth ) ) {
4775 out . push ( DETAIL_INDENT + chalk . hex ( this . colors . textDim ) ( line ) ) ;
4876 }
49- return out ;
77+ return this . withLeadingBlank ( out ) ;
78+ }
79+
80+ private withLeadingBlank ( lines : string [ ] ) : string [ ] {
81+ return this . leadingBlank ? [ '' , ...lines ] : lines ;
5082 }
5183}
5284
@@ -59,24 +91,37 @@ export function buildGoalMarker(
5991 change : GoalChange ,
6092 colors : ColorPalette ,
6193 expanded : boolean ,
94+ actor ?: GoalMarkerActor ,
6295) : GoalMarkerComponent | null {
63- const spec = markerSpec ( change , colors ) ;
96+ const spec = markerSpec ( change , colors , actor ) ;
6497 if ( spec === null ) return null ;
65- const marker = new GoalMarkerComponent ( spec . headline , change . reason , colors , spec . accentHex ) ;
98+ const marker = new GoalMarkerComponent (
99+ spec . headline ,
100+ spec . detail ?? change . reason ,
101+ colors ,
102+ spec . accentHex ,
103+ spec . options ,
104+ ) ;
66105 marker . setExpanded ( expanded ) ;
67106 return marker ;
68107}
69108
70109function markerSpec (
71110 change : GoalChange ,
72111 colors : ColorPalette ,
73- ) : { headline : string ; accentHex : string } | null {
112+ actor ?: GoalMarkerActor ,
113+ ) : {
114+ headline : string ;
115+ accentHex : string ;
116+ detail ?: string | undefined ;
117+ options ?: GoalMarkerOptions | undefined ;
118+ } | null {
74119 if ( change . kind === 'lifecycle' ) {
75120 switch ( change . status ) {
76121 case 'paused' :
77- return { headline : 'Goal paused' , accentHex : colors . textDim } ;
122+ return prominentMarker ( pausedHeadline ( change . reason , actor ) , colors . warning ) ;
78123 case 'active' :
79- return { headline : 'Goal resumed' , accentHex : colors . primary } ;
124+ return prominentMarker ( resumedHeadline ( actor ) , colors . primary ) ;
80125 case 'blocked' :
81126 // The system stopped pursuing the goal; resumable via `/goal resume`.
82127 return { headline : 'Goal blocked' , accentHex : colors . warning } ;
@@ -87,6 +132,40 @@ function markerSpec(
87132 return null ; // completion -> posts its own message, not a marker
88133}
89134
135+ function prominentMarker ( headline : string , accentHex : string ) {
136+ return {
137+ headline,
138+ accentHex,
139+ detail : undefined ,
140+ options : {
141+ marker : STATUS_BULLET . trimEnd ( ) ,
142+ textHex : accentHex ,
143+ expandable : false ,
144+ indent : '' ,
145+ leadingBlank : true ,
146+ } ,
147+ } ;
148+ }
149+
150+ function pausedHeadline ( reason : string | undefined , actor : GoalMarkerActor | undefined ) : string {
151+ if ( reason === 'Paused after interruption' ) return "Goal paused due to user's interruption" ;
152+ if ( actor === 'user' ) return 'Goal paused by the user.' ;
153+ if ( reason ?. startsWith ( 'Paused ' ) === true ) return `Goal ${ lowercaseFirst ( reason ) } ` ;
154+ if ( reason !== undefined && reason . length > 0 ) return `Goal paused: ${ reason } ` ;
155+ if ( actor === 'model' ) return 'Goal paused by the agent.' ;
156+ return 'Goal paused' ;
157+ }
158+
159+ function resumedHeadline ( actor : GoalMarkerActor | undefined ) : string {
160+ if ( actor === 'user' ) return 'Goal resumed by the user.' ;
161+ if ( actor === 'model' ) return 'Goal resumed by the agent.' ;
162+ return 'Goal resumed' ;
163+ }
164+
165+ function lowercaseFirst ( text : string ) : string {
166+ return text . length === 0 ? text : `${ text [ 0 ] ! . toLowerCase ( ) } ${ text . slice ( 1 ) } ` ;
167+ }
168+
90169function wrap ( text : string , width : number ) : string [ ] {
91170 const words = text . replace ( / \s + / g, ' ' ) . trim ( ) . split ( ' ' ) ;
92171 const lines : string [ ] = [ ] ;
0 commit comments