Skip to content

Commit e83e48c

Browse files
committed
feat(copilot-ui): no more prefix spam, single green assistant label
- Introduce ASSISTANT_LABEL, push_assistant_block, preserve newlines - Unifies reasoning, info, errors, tool output, first-line prefix only Signed-off-by: Jessie Frazelle <[email protected]>
1 parent c6d4e6a commit e83e48c

File tree

1 file changed

+50
-36
lines changed

1 file changed

+50
-36
lines changed

src/ml/copilot/ui.rs

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,36 @@ use ratatui::{prelude::*, widgets::*};
22

33
use super::state::{App, ChatEvent};
44

5+
const ASSISTANT_LABEL: &str = "ML-ephant> ";
56
const 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.
836
fn 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

Comments
 (0)