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:
Hunter Bown authored and GitHub committed 2026-05-08 10:15:43 -05:00
1 parent 37178ed6b4
commit caf77949d9
2 files changed
+131 -40

No files matched your search

+36 -4
View File
@@ -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
+95 -36
View File
@@ -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