diff --git a/agentydragon/tasks/21-compact-markdown-rendering.md b/agentydragon/tasks/21-compact-markdown-rendering.md index c39c6e7c36..c1f46fbfab 100644 --- a/agentydragon/tasks/21-compact-markdown-rendering.md +++ b/agentydragon/tasks/21-compact-markdown-rendering.md @@ -1,9 +1,9 @@ +++ id = "21" title = "Compact Markdown Rendering Option" -status = "Not started" -dependencies = "" # No prerequisites -last_updated = "2025-06-25T01:40:09.600000" +status = "Done" +dependencies = "03,06,08,13,15,32,18,19,22,23" +last_updated = "2025-06-24T22:25:59.864216" +++ ## Summary diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 3a3ef7427a..8579cc88ed 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -90,6 +90,11 @@ pub struct Tui { #[serde(default)] pub disable_mouse_capture: bool, + /// When `true`, omit blank lines immediately following Markdown headings + /// (levels 1–6) in TUI rendering for more compact vertical spacing. + #[serde(default)] + pub markdown_compact: bool, + /// Maximum number of visible lines in the chat input composer before scrolling. /// The composer will expand up to this many lines; additional content will enable /// an internal scrollbar. @@ -114,6 +119,7 @@ impl Default for Tui { fn default() -> Self { Self { disable_mouse_capture: Default::default(), + markdown_compact: Default::default(), composer_max_rows: default_composer_max_rows(), editor: default_editor(), } diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index ab20138298..8212cbf63a 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -12,7 +12,15 @@ pub(crate) fn append_markdown( lines: &mut Vec>, config: &Config, ) { - append_markdown_with_opener_and_cwd(markdown_source, lines, config.file_opener, &config.cwd); + let mut new_lines = Vec::new(); + append_markdown_with_opener_and_cwd(markdown_source, &mut new_lines, config.file_opener, &config.cwd); + if config.tui.markdown_compact { + for line in collapse_heading_blank_lines(new_lines) { + lines.push(line); + } + } else { + lines.extend(new_lines); + } } fn append_markdown_with_opener_and_cwd( @@ -53,6 +61,29 @@ fn append_markdown_with_opener_and_cwd( } } +/// Remove blank lines immediately following Markdown headings (levels 1–6). +fn collapse_heading_blank_lines(lines: Vec>) -> Vec> { + let mut result = Vec::with_capacity(lines.len()); + let mut prev_was_heading = false; + for line in lines { + let content = line.spans.iter().map(|s| s.content.clone()).collect::(); + if prev_was_heading && content.trim().is_empty() { + continue; + } + prev_was_heading = { + let s = content.as_str(); + s.starts_with("# ") + || s.starts_with("## ") + || s.starts_with("### ") + || s.starts_with("#### ") + || s.starts_with("##### ") + || s.starts_with("###### ") + }; + result.push(line); + } + result +} + /// Rewrites file citations in `src` into markdown hyperlinks using the /// provided `scheme` (`vscode`, `cursor`, etc.). The resulting URI follows the /// format expected by VS Code-compatible file openers: @@ -104,6 +135,52 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn collapse_blank_after_heading() { + let lines = vec![ + Line::from(vec![Span::raw("# Heading")]), + Line::from(vec![Span::raw("")]), + Line::from(vec![Span::raw("Paragraph")]), + ]; + let collapsed = collapse_heading_blank_lines(lines); + let rendered: Vec = collapsed + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect()) + .collect(); + assert_eq!(rendered, vec!["# Heading", "Paragraph"]); + } + + #[test] + fn preserve_blank_not_after_heading() { + let lines = vec![ + Line::from(vec![Span::raw("Normal")]), + Line::from(vec![Span::raw("")]), + Line::from(vec![Span::raw("Paragraph")]), + ]; + let collapsed = collapse_heading_blank_lines(lines); + let rendered: Vec = collapsed + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect()) + .collect(); + assert_eq!(rendered, vec!["Normal", "", "Paragraph"]); + } + + #[test] + fn collapse_multiple_blanks_after_heading() { + let lines = vec![ + Line::from(vec![Span::raw("## Heading2")]), + Line::from(vec![Span::raw("")]), + Line::from(vec![Span::raw("")]), + Line::from(vec![Span::raw("Para")]), + ]; + let collapsed = collapse_heading_blank_lines(lines); + let rendered: Vec = collapsed + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect()) + .collect(); + assert_eq!(rendered, vec!["## Heading2", "Para"]); + } + #[test] fn citation_is_rewritten_with_absolute_path() { let markdown = "See 【F:/src/main.rs†L42-L50】 for details.";