@@ -2,8 +2,36 @@ use ratatui::{prelude::*, widgets::*};
22
33use super :: state:: { App , ChatEvent } ;
44
5+ const ASSISTANT_LABEL : & str = "ML-ephant> " ;
56const ASSISTANT_INDENT : & str = " " ; // 4 spaces for a pleasant left gutter
67
8+ fn push_assistant_block < ' a > (
9+ lines : & mut Vec < Line < ' a > > ,
10+ parts : Vec < String > ,
11+ style : Option < Style > ,
12+ first_line_prefix : Option < Span < ' a > > ,
13+ ) {
14+ if parts. is_empty ( ) {
15+ return ;
16+ }
17+ for ( i, part) in parts. into_iter ( ) . enumerate ( ) {
18+ let mut spans: Vec < Span > = Vec :: new ( ) ;
19+ if i == 0 {
20+ spans. push ( Span :: styled ( ASSISTANT_LABEL , Style :: default ( ) . fg ( Color :: Green ) ) ) ;
21+ if let Some ( pref) = & first_line_prefix {
22+ spans. push ( pref. clone ( ) ) ;
23+ }
24+ } else {
25+ spans. push ( Span :: raw ( ASSISTANT_INDENT ) ) ;
26+ }
27+ match style {
28+ Some ( st) => spans. push ( Span :: styled ( part, st) ) ,
29+ None => spans. push ( Span :: raw ( part) ) ,
30+ }
31+ lines. push ( Line :: from ( spans) ) ;
32+ }
33+ }
34+
735// Very simple renderer that preserves newlines exactly as provided.
836fn render_preserving_newlines ( s : & str ) -> Vec < String > {
937 // `split('\n')` keeps trailing empty segments, which is what we want
@@ -134,11 +162,8 @@ pub fn draw(frame: &mut Frame, app: &App) {
134162 match ev {
135163 ChatEvent :: User ( s) => {
136164 if !assistant_buf. is_empty ( ) {
137- // Flush any pending assistant text without a label; keep a small left indent.
138- lines. push ( Line :: from ( vec ! [
139- Span :: raw( ASSISTANT_INDENT ) ,
140- Span :: raw( assistant_buf. clone( ) ) ,
141- ] ) ) ;
165+ let rows = render_preserving_newlines ( & assistant_buf) ;
166+ push_assistant_block ( & mut lines, rows, None , None ) ;
142167 assistant_buf. clear ( ) ;
143168 }
144169 lines. push ( Line :: from ( vec ! [
@@ -152,54 +177,43 @@ pub fn draw(frame: &mut Frame, app: &App) {
152177 }
153178 kittycad:: types:: MlCopilotServerMessage :: EndOfStream { .. } => {
154179 if !assistant_buf. is_empty ( ) {
155- for l in render_preserving_newlines ( & assistant_buf) {
156- lines. push ( Line :: from ( vec ! [ Span :: raw( ASSISTANT_INDENT ) , Span :: raw( l) ] ) ) ;
157- }
180+ let rows = render_preserving_newlines ( & assistant_buf) ;
181+ push_assistant_block ( & mut lines, rows, None , None ) ;
158182 assistant_buf. clear ( ) ;
159183 }
160184 }
161185 kittycad:: types:: MlCopilotServerMessage :: Reasoning ( reason) => {
162- // Render reasoning as dimmed markdown lines for readability .
186+ // Render reasoning as dimmed markdown lines with a single label .
163187 let md = crate :: context:: reasoning_to_markdown ( reason) ;
164- for l in render_markdown_to_lines ( & md) {
165- lines. push ( Line :: from ( vec ! [
166- Span :: raw( ASSISTANT_INDENT ) ,
167- Span :: styled( l, Style :: default ( ) . fg( Color :: Rgb ( 150 , 150 , 150 ) ) ) ,
168- ] ) ) ;
169- }
188+ let rows = render_markdown_to_lines ( & md) ;
189+ push_assistant_block (
190+ & mut lines,
191+ rows,
192+ Some ( Style :: default ( ) . fg ( Color :: Rgb ( 150 , 150 , 150 ) ) ) ,
193+ None ,
194+ ) ;
170195 }
171196 kittycad:: types:: MlCopilotServerMessage :: Info { text } => {
172- // Render info text as markdown, split into lines; print each on its own row.
173- for part in render_markdown_to_lines ( text) {
174- lines. push ( Line :: from ( vec ! [ Span :: raw( ASSISTANT_INDENT ) , Span :: raw( part) ] ) ) ;
175- }
197+ let rows = render_markdown_to_lines ( text) ;
198+ push_assistant_block ( & mut lines, rows, None , None ) ;
176199 }
177200 kittycad:: types:: MlCopilotServerMessage :: Error { detail } => {
178- for part in detail. split ( '\n' ) {
179- lines. push ( Line :: from ( vec ! [
180- Span :: raw( ASSISTANT_INDENT ) ,
181- Span :: styled( part. to_string( ) , Style :: default ( ) . fg( Color :: Red ) ) ,
182- ] ) ) ;
183- }
201+ let rows: Vec < String > = detail. split ( '\n' ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
202+ push_assistant_block ( & mut lines, rows, Some ( Style :: default ( ) . fg ( Color :: Red ) ) , None ) ;
184203 }
185204 kittycad:: types:: MlCopilotServerMessage :: ToolOutput { result } => {
186205 let raw = format ! ( "{result:#?}" ) ;
187- for part in raw. split ( '\n' ) {
188- lines. push ( Line :: from ( vec ! [
189- Span :: raw( ASSISTANT_INDENT ) ,
190- Span :: styled( "tool output → " , Style :: default ( ) . fg( Color :: Yellow ) ) ,
191- Span :: raw( part. to_string( ) ) ,
192- ] ) ) ;
193- }
206+ let rows: Vec < String > = raw. split ( '\n' ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
207+ let prefix = Span :: styled ( "tool output → " , Style :: default ( ) . fg ( Color :: Yellow ) ) ;
208+ push_assistant_block ( & mut lines, rows, None , Some ( prefix) ) ;
194209 }
195210 } ,
196211 }
197212 }
198213 if !assistant_buf. is_empty ( ) {
199- // Live-render preserving newlines exactly
200- for l in render_preserving_newlines ( & assistant_buf) {
201- lines. push ( Line :: from ( vec ! [ Span :: raw( ASSISTANT_INDENT ) , Span :: raw( l) ] ) ) ;
202- }
214+ // Live-render preserving newlines exactly, with a single label at the start
215+ let rows = render_preserving_newlines ( & assistant_buf) ;
216+ push_assistant_block ( & mut lines, rows, None , None ) ;
203217 }
204218 if app. pending_edits . is_none ( ) {
205219 let messages = Paragraph :: new ( lines)
0 commit comments