@@ -20,27 +20,24 @@ import { ansi, markdownTheme } from "./theme.js";
2020
2121const wrap = ( text : string , width : number ) => wrapTextWithAnsi ( text , width ) ;
2222
23+ export type MessageKind = "user" | "assistant" | "tool" | "system" ;
24+
2325/**
24- * Single transcript row with a colored "│ " gutter on every line, role-
25- * colored bold label header, then content blocks underneath. Mirrors
26- * ink-era Message.tsx — the vertical accent gutter is the strongest
27- * visual signal in the chat surface, so the pi-tui path needs it too.
26+ * A transcript turn. The speaker is conveyed by *treatment*, not a text
27+ * label: the user's words sit behind a solid accent bar (a quoted "card"),
28+ * everything the agent produces — prose, tool calls, results — flows as
29+ * open, indented text. No "you:" / "codebase:" headers, so consecutive
30+ * agent turns read as one continuous voice instead of a stutter of labels.
2831 *
29- * Content blocks are stored as pi-tui Components; the gutter is added
30- * here at render time by wrapping every child line with the accent
31- * prefix. This means Markdown / Text / ToolCallLine all keep their
32- * normal line output and we don't have to know their internals.
32+ * Every block keeps its own line output; we only prepend the per-line
33+ * prefix here, so Markdown / Text / ToolCallLine stay oblivious.
3334 */
3435export class MessageView implements Component {
35- private readonly accent : ( text : string ) => string ;
36- private readonly label : string ;
37- private readonly streaming : boolean ;
36+ private readonly kind : MessageKind ;
3837 private blocks : Component [ ] ;
3938
40- constructor ( opts : { accent : ( s : string ) => string ; label : string ; streaming : boolean ; blocks : Component [ ] } ) {
41- this . accent = opts . accent ;
42- this . label = opts . label ;
43- this . streaming = opts . streaming ;
39+ constructor ( opts : { kind : MessageKind ; streaming ?: boolean ; blocks : Component [ ] } ) {
40+ this . kind = opts . kind ;
4441 this . blocks = opts . blocks ;
4542 }
4643
@@ -49,13 +46,13 @@ export class MessageView implements Component {
4946 }
5047
5148 render ( width : number ) : string [ ] {
49+ // User turns get a bold accent bar; agent/tool/system turns get a
50+ // flat 2-col indent. Both consume 2 columns, so content reflows the same.
51+ const prefix = this . kind === "user" ? `${ ansi . bold ( ansi . cyan ( "▌" ) ) } ` : " " ;
5252 const innerWidth = Math . max ( 20 , width - 2 ) ;
53- const gutter = this . accent ( "│ " ) ;
5453 const out : string [ ] = [ ] ;
55- out . push ( `${ gutter } ${ this . accent ( ansi . bold ( this . label ) ) } ${ this . streaming ? ansi . dim ( " …" ) : "" } ` ) ;
5654 for ( const block of this . blocks ) {
57- const childLines = block . render ( innerWidth ) ;
58- for ( const line of childLines ) out . push ( `${ gutter } ${ line } ` ) ;
55+ for ( const line of block . render ( innerWidth ) ) out . push ( `${ prefix } ${ line } ` ) ;
5956 }
6057 out . push ( "" ) ;
6158 return out ;
@@ -90,14 +87,17 @@ export class ToolCallLine implements Component {
9087 if ( status === "running" ) {
9188 const frame = SPINNER_FRAMES [ Math . floor ( Date . now ( ) / 90 ) % SPINNER_FRAMES . length ] ;
9289 const label = toolActionLabel ( this . name , this . args ) ;
93- return wrap ( `${ frame } ${ label } …` , width ) . map ( ( l ) => ansi . magenta ( l ) ) ;
90+ return wrap ( `${ frame } ${ label } …` , width ) . map ( ( l ) => ansi . cyan ( l ) ) ;
9491 }
9592
93+ // Completed tools recede into dim narrative so the eye stays on the
94+ // agent's prose; only the status glyph keeps a touch of color.
9695 const isError = status === "error" ;
97- const glyph = isError ? "✗" : "✓" ;
98- const color = isError ? ansi . red : ansi . magenta ;
96+ const glyph = isError ? ansi . red ( "✗" ) : ansi . green ( "✓" ) ;
97+ const color = isError ? ansi . red : ansi . dim ;
9998 const past = toolActionPast ( this . name , this . args ) ;
100- const lines = wrap ( `${ glyph } ${ past } ` , width ) . map ( ( l ) => color ( l ) ) ;
99+ const wrapped = wrap ( past , Math . max ( 10 , width - 2 ) ) ;
100+ const lines = wrapped . map ( ( l , idx ) => ( idx === 0 ? `${ glyph } ${ color ( l ) } ` : ` ${ color ( l ) } ` ) ) ;
101101
102102 // Diff summary for edits — indented under the tool-call line so
103103 // the read flows top-to-bottom: "what just happened" then "what
@@ -139,18 +139,20 @@ export class CollapsedReadGroup implements Component {
139139 const doneCount = statuses . filter ( ( s ) => s !== "running" ) . length ;
140140
141141 const glyph = anyRunning
142- ? SPINNER_FRAMES [ Math . floor ( Date . now ( ) / 90 ) % SPINNER_FRAMES . length ]
142+ ? ansi . cyan ( SPINNER_FRAMES [ Math . floor ( Date . now ( ) / 90 ) % SPINNER_FRAMES . length ] )
143143 : anyError
144- ? "✗"
145- : "✓" ;
146- const color = anyError ? ansi . red : ansi . magenta ;
144+ ? ansi . red ( "✗" )
145+ : ansi . green ( "✓" ) ;
146+ const color = anyError ? ansi . red : anyRunning ? ansi . cyan : ansi . dim ;
147147 const verb = anyRunning ? presentVerbForReadTool ( this . toolName ) : pastVerbForReadTool ( this . toolName ) ;
148148 const noun = nounForReadTool ( this . toolName , this . calls . length ) ;
149149 const header = anyRunning
150- ? `${ glyph } ${ verb } ${ doneCount } of ${ this . calls . length } ${ noun } …`
151- : `${ glyph } ${ verb } ${ this . calls . length } ${ noun } ` ;
150+ ? `${ verb } ${ doneCount } of ${ this . calls . length } ${ noun } …`
151+ : `${ verb } ${ this . calls . length } ${ noun } ` ;
152152
153- const lines = wrap ( header , width ) . map ( ( l ) => color ( l ) ) ;
153+ const lines = wrap ( header , Math . max ( 10 , width - 2 ) ) . map ( ( l , idx ) =>
154+ idx === 0 ? `${ glyph } ${ color ( l ) } ` : ` ${ color ( l ) } ` ,
155+ ) ;
154156 const pathWidth = Math . max ( 20 , width - 6 ) ;
155157 for ( const c of this . calls ) {
156158 const a = ( c . args ?? { } ) as Record < string , unknown > ;
0 commit comments