mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-13 03:21:33 -10:00
fix(tui): hide transcript rail before code blocks
## Summary - carry markdown render metadata through the message renderer so fenced code lines are identified explicitly - keep normal conversational continuation rails for prose, but replace the rail with alignment spaces before fenced code lines - add a regression test for assistant code blocks so the visible U+258F rail does not appear inside code output ## Test plan - cargo test -p deepseek-tui assistant_code_block_lines_do_not_get_transcript_rail --locked - cargo fmt --all -- --check - git diff --check origin/main..HEAD - GitHub CI: lint, version drift, ubuntu/macos/windows tests, npm wrapper smoke, GitGuardian
This commit is contained in:
1 parent
37178ed6b4
commit
caf77949d9
2 files changed
+131
-40
No files matched your search
@@ -2144,8 +2144,9 @@ fn render_message(
|
||||
let prefix_width_u16 = u16::try_from(prefix_width.saturating_add(2)).unwrap_or(u16::MAX);
|
||||
let content_width = usize::from(width.saturating_sub(prefix_width_u16).max(1));
|
||||
let mut lines = Vec::new();
|
||||
let rendered = markdown_render::render_markdown(content, content_width as u16, body_style);
|
||||
for (idx, line) in rendered.into_iter().enumerate() {
|
||||
let rendered =
|
||||
markdown_render::render_markdown_tagged(content, content_width as u16, body_style);
|
||||
for (idx, rendered_line) in rendered.into_iter().enumerate() {
|
||||
if idx == 0 {
|
||||
let mut spans = Vec::new();
|
||||
if !prefix.is_empty() {
|
||||
@@ -2155,11 +2156,13 @@ fn render_message(
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
spans.extend(line.spans);
|
||||
spans.extend(rendered_line.line.spans);
|
||||
lines.push(Line::from(spans));
|
||||
} else {
|
||||
let indent = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else if rendered_line.is_code {
|
||||
" ".repeat(prefix_width + 1)
|
||||
} else {
|
||||
let mut s = String::with_capacity(prefix_width + 1);
|
||||
s.push('\u{258F}');
|
||||
@@ -2168,7 +2171,7 @@ fn render_message(
|
||||
};
|
||||
let rail_style = Style::default().fg(palette::TEXT_DIM);
|
||||
let mut spans = vec![Span::styled(indent, rail_style)];
|
||||
spans.extend(line.spans);
|
||||
spans.extend(rendered_line.line.spans);
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
}
|
||||
@@ -3648,6 +3651,35 @@ mod tests {
|
||||
assert!(visible.contains("ready"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assistant_code_block_lines_do_not_get_transcript_rail() {
|
||||
let cell = HistoryCell::Assistant {
|
||||
content: "SQL:\n```sql\nSELECT\nFROM customers\n```".to_string(),
|
||||
streaming: false,
|
||||
};
|
||||
let visible: Vec<String> = cell
|
||||
.lines(80)
|
||||
.iter()
|
||||
.map(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.content.as_ref())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(visible[0], format!("{ASSISTANT_GLYPH} SQL:"));
|
||||
for line in visible
|
||||
.iter()
|
||||
.filter(|line| line.contains("SELECT") || line.contains("FROM customers"))
|
||||
{
|
||||
assert!(
|
||||
!line.contains('\u{258F}'),
|
||||
"code block line should not inherit the transcript rail: {line:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assistant_glyph_holds_full_brightness_when_idle() {
|
||||
// Idle (streaming=false) and low_motion both pin the colour to the
|
||||
|
||||
@@ -92,6 +92,17 @@ pub struct ParsedMarkdown {
|
||||
blocks: Vec<Block>,
|
||||
}
|
||||
|
||||
/// Width-dependent rendered line plus the source block kind that produced it.
|
||||
///
|
||||
/// Most callers only need styled terminal lines, but transcript rendering also
|
||||
/// needs to avoid adding its conversational continuation rail in front of code
|
||||
/// blocks. Keeping this metadata here avoids guessing from styled spans.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedMarkdownLine {
|
||||
pub line: Line<'static>,
|
||||
pub is_code: bool,
|
||||
}
|
||||
|
||||
/// Parse markdown source into a width-independent block AST.
|
||||
///
|
||||
/// This is a small line-oriented parser tuned for the patterns we render:
|
||||
@@ -178,8 +189,21 @@ pub fn parse(content: &str) -> ParsedMarkdown {
|
||||
/// skip the parse step entirely.
|
||||
#[must_use]
|
||||
pub fn render_parsed(parsed: &ParsedMarkdown, width: u16, base_style: Style) -> Vec<Line<'static>> {
|
||||
render_parsed_tagged(parsed, width, base_style)
|
||||
.into_iter()
|
||||
.map(|line| line.line)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render a parsed-markdown AST and preserve per-line source metadata.
|
||||
#[must_use]
|
||||
pub fn render_parsed_tagged(
|
||||
parsed: &ParsedMarkdown,
|
||||
width: u16,
|
||||
base_style: Style,
|
||||
) -> Vec<RenderedMarkdownLine> {
|
||||
let width = width.max(1) as usize;
|
||||
let mut out: Vec<Line<'static>> = Vec::with_capacity(parsed.blocks.len());
|
||||
let mut out: Vec<RenderedMarkdownLine> = Vec::with_capacity(parsed.blocks.len());
|
||||
|
||||
let mut i = 0;
|
||||
while i < parsed.blocks.len() {
|
||||
@@ -196,11 +220,14 @@ pub fn render_parsed(parsed: &ParsedMarkdown, width: u16, base_style: Style) ->
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
out.extend(render_table_group(
|
||||
&parsed.blocks[start..i],
|
||||
width,
|
||||
base_style,
|
||||
));
|
||||
out.extend(
|
||||
render_table_group(&parsed.blocks[start..i], width, base_style)
|
||||
.into_iter()
|
||||
.map(|line| RenderedMarkdownLine {
|
||||
line,
|
||||
is_code: false,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -209,44 +236,63 @@ pub fn render_parsed(parsed: &ParsedMarkdown, width: u16, base_style: Style) ->
|
||||
let style = Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
out.extend(render_wrapped_line(text, width, style, false));
|
||||
out.extend(render_wrapped_line_tagged(text, width, style, false, false));
|
||||
}
|
||||
Block::HeadingRule => {
|
||||
out.push(Line::from(Span::styled(
|
||||
"─".repeat(width.min(40)),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
)));
|
||||
out.push(RenderedMarkdownLine {
|
||||
line: Line::from(Span::styled(
|
||||
"─".repeat(width.min(40)),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
)),
|
||||
is_code: false,
|
||||
});
|
||||
}
|
||||
Block::HorizontalRule => {
|
||||
out.push(Line::from(Span::styled(
|
||||
"─".repeat(width.min(60)),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
)));
|
||||
out.push(RenderedMarkdownLine {
|
||||
line: Line::from(Span::styled(
|
||||
"─".repeat(width.min(60)),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
)),
|
||||
is_code: false,
|
||||
});
|
||||
}
|
||||
Block::ListItem { bullet, text } => {
|
||||
let bullet_style = Style::default().fg(palette::DEEPSEEK_SKY);
|
||||
out.extend(render_list_line(
|
||||
bullet,
|
||||
text,
|
||||
width,
|
||||
bullet_style,
|
||||
base_style,
|
||||
));
|
||||
out.extend(
|
||||
render_list_line(bullet, text, width, bullet_style, base_style)
|
||||
.into_iter()
|
||||
.map(|line| RenderedMarkdownLine {
|
||||
line,
|
||||
is_code: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Block::Code { line } => {
|
||||
let code_style = Style::default()
|
||||
.fg(palette::DEEPSEEK_SKY)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
out.extend(render_wrapped_line(line, width, code_style, true));
|
||||
out.extend(render_wrapped_line_tagged(
|
||||
line, width, code_style, true, true,
|
||||
));
|
||||
}
|
||||
Block::Paragraph { text } => {
|
||||
let link_style = Style::default()
|
||||
.fg(palette::DEEPSEEK_BLUE)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
out.extend(render_line_with_links(text, width, base_style, link_style));
|
||||
out.extend(
|
||||
render_line_with_links(text, width, base_style, link_style)
|
||||
.into_iter()
|
||||
.map(|line| RenderedMarkdownLine {
|
||||
line,
|
||||
is_code: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Block::Blank => {
|
||||
out.push(Line::from(""));
|
||||
out.push(RenderedMarkdownLine {
|
||||
line: Line::from(""),
|
||||
is_code: false,
|
||||
});
|
||||
}
|
||||
Block::TableRow(_) | Block::TableSeparator => unreachable!(),
|
||||
}
|
||||
@@ -254,7 +300,10 @@ pub fn render_parsed(parsed: &ParsedMarkdown, width: u16, base_style: Style) ->
|
||||
}
|
||||
|
||||
if out.is_empty() {
|
||||
out.push(Line::from(""));
|
||||
out.push(RenderedMarkdownLine {
|
||||
line: Line::from(""),
|
||||
is_code: false,
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
@@ -271,6 +320,17 @@ pub fn render_markdown(content: &str, width: u16, base_style: Style) -> Vec<Line
|
||||
render_parsed(&parsed, width, base_style)
|
||||
}
|
||||
|
||||
/// Convenience wrapper: parse + render while keeping per-line source metadata.
|
||||
#[must_use]
|
||||
pub fn render_markdown_tagged(
|
||||
content: &str,
|
||||
width: u16,
|
||||
base_style: Style,
|
||||
) -> Vec<RenderedMarkdownLine> {
|
||||
let parsed = parse(content);
|
||||
render_parsed_tagged(&parsed, width, base_style)
|
||||
}
|
||||
|
||||
fn parse_heading(line: &str) -> Option<(usize, &str)> {
|
||||
let trimmed = line.trim_start();
|
||||
let hashes = trimmed.chars().take_while(|c| *c == '#').count();
|
||||
@@ -305,12 +365,13 @@ fn parse_list_item(line: &str) -> Option<(String, &str)> {
|
||||
Some((format!("{}.", &trimmed[..idx]), rest.trim_start()))
|
||||
}
|
||||
|
||||
fn render_wrapped_line(
|
||||
fn render_wrapped_line_tagged(
|
||||
line: &str,
|
||||
width: usize,
|
||||
style: Style,
|
||||
indent_code: bool,
|
||||
) -> Vec<Line<'static>> {
|
||||
is_code: bool,
|
||||
) -> Vec<RenderedMarkdownLine> {
|
||||
let prefix = if indent_code { " " } else { "" };
|
||||
let prefix_width = prefix.width();
|
||||
let available = width.saturating_sub(prefix_width).max(1);
|
||||
@@ -324,17 +385,15 @@ fn render_wrapped_line(
|
||||
let mut out = Vec::new();
|
||||
|
||||
for (idx, chunk) in wrapped.into_iter().enumerate() {
|
||||
if idx == 0 {
|
||||
out.push(Line::from(vec![
|
||||
Span::raw(prefix),
|
||||
Span::styled(chunk, style),
|
||||
]));
|
||||
let line = if idx == 0 {
|
||||
Line::from(vec![Span::raw(prefix), Span::styled(chunk, style)])
|
||||
} else {
|
||||
out.push(Line::from(vec![
|
||||
Line::from(vec![
|
||||
Span::raw(" ".repeat(prefix_width)),
|
||||
Span::styled(chunk, style),
|
||||
]));
|
||||
}
|
||||
])
|
||||
};
|
||||
out.push(RenderedMarkdownLine { line, is_code });
|
||||
}
|
||||
|
||||
out
|
||||
|
||||
Reference in new issue
Block a user