feat(rich_output): block-level and inline markdown rendering for LLM responses (PR2) - #4575
Closed
KUSH42 wants to merge 8 commits into
Closed
feat(rich_output): block-level and inline markdown rendering for LLM responses (PR2)#4575KUSH42 wants to merge 8 commits into
KUSH42 wants to merge 8 commits into
Conversation
Introduces agent/rich_output.py — a self-contained Rich/Pygments rendering toolkit with no project-specific imports. Public API: - LanguageDetector: extension map + content-pattern heuristics - SyntaxHighlighter: Pygments → Rich markup → ANSI string - FilePathFormatter: per-filetype icons, compact relative paths - DiffRenderer: unified diff → Rich Text with line numbers - clean_command_output: strip venv/stacktrace noise from command output DiffRenderer replaces _render_inline_unified_diff in display.py: - Intra-line character-level highlighting via SequenceMatcher (threshold 0.5) - Per-run del/add pairing to avoid cross-hunk false matches - Summary header: ● filename.py Added N lines, removed M lines - Console width from shutil.get_terminal_size, not hardcoded Tests: 51 passing in tests/test_rich_output.py
Pygments emits Error tokens for content its markdown lexer cannot tokenize (emoji in headings, unknown syntax, etc.). Mapping Error to "bold red on red" matched the diff-deletion colour, causing spurious red backgrounds on unrelated text. Changed to "bold red" (text colour only), consistent with Generic.Error.
Wires the SyntaxHighlighter/LanguageDetector from rich_output.py into tool result display and LLM response rendering. execute_code preview: - Highlighted Python block printed after successful execution - Gated on _result_succeeded — nothing shown after a failed run - Cute-msg drops the inline snippet when highlight is active (no duplication) read_file preview: - ┊ 📄 filename.py header + syntax-highlighted content - Language from extension only; unknown types skipped silently terminal preview: - Verb-based language detection from the command - _FILE_EXEC_COMMANDS blocklist (node, python3, bash, …) prevents runtime stdout from being mistaken for source code LLM response rendering: - format_response() highlights fenced code blocks in complete responses - StreamingCodeBlockHighlighter state machine for streaming: buffers fenced blocks, flushes highlighted on closing fence, plain text passes through immediately with response text colour preserved Plumbing: - Verbosity gate: all previews suppressed when tool_progress_mode == "off" - display.code_highlight config key + /code-highlight toggle - set_code_highlight_active() keeps display.py decoupled from CLI state - Module-level _rich_detector singleton (no per-call instantiation) Tests: 135 passing (tests/test_display.py + tests/test_rich_output.py)
format_response wrapped highlighted code in ``` delimiters under the theory that "the Panel still looks like a code block". In practice the ANSI-highlighted block reads cleanly without them, and keeping the fences caused raw backtick lines to appear in the rendered response.
_number_code_lines() prepends dim right-justified line numbers with a │ separator to every line of a highlighted code block: 1 │ import requests 2 │ import time ... Called from both format_response._highlight (batch path) and StreamingCodeBlockHighlighter._flush_block (streaming path) so line numbers appear consistently in all LLM response code blocks.
…endering bugs _PygmentsToRich.format() previously escaped brackets by replacing: [ → \[ (correct: literal "[" in Rich markup) ] → \] (WRONG: Rich has no \] escape; renders as literal \]) This caused two bugs: 1. Haskell type signatures like [Integer] rendered as [Integer\] 2. A trailing \ token (e.g. Haskell lambda \a) before a close tag like [/bold yellow] formed the Rich escape \[ — consuming the closing tag and leaking the tag text into the output. Fix: use rich.markup.escape() which doubles \ → \\ and escapes [ → \[ while leaving ] alone (] requires no escaping in Rich markup). Apply to ALL tokens (styled and unstyled) and to all fallback paths. Also add _highlight_inline_code() and format_response() for inline code span styling in LLM response text.
…responses Adds apply_block_line (headings, hr, blockquotes, lists, ref-link suppression) and extends apply_inline_markdown (images, links, HTML inline tags). format_response gains a pass-2 that applies both renderers to every non-highlighted line. The streaming path chains the same pair in _emit_stream_text and _flush_stream. Bug fixes included: - _code_highlight_active was gating apply_block_line/apply_inline_markdown in the streaming path; removed the guard (_RICH_RESPONSE is the correct gate; _code_highlight_active controls only tool-output highlighting) - _MD_ITALIC_UNDER_RE rejected phrases with spaces; changed [^_\s\n]+ to [^_\n]+ (word-boundary lookbehind prevents snake_case false positives) - _MD_REF_LINK_RE had a $ anchor that blocked titled reference-link definitions from being suppressed; removed $ - Blockquote inline spans reset to terminal default; added reset_suffix to the apply_inline_markdown call in the blockquote branch - format_response splitlines(keepends=True) fed \n into capture groups, silently dropping block elements; switched to splitlines() + manual join - CommonMark backslash escapes (\] → ]) were passed through literally; added step-7 re.sub pass in apply_inline_markdown - Pygments plain-text lexer emits lines with no ANSI codes; pass-2 guard "\x1b" in l then fell through and applied markdown to code-fence content (### was stripped, ** rendered, etc.); _highlight now prepends \x1b[0m to bare lines so the guard is always satisfied
…e correctness - Anchor code-fence regex to line start ((?m)^) so fences prefixed with > are not incorrectly consumed as code block openers; previously caused blockquote lines to be syntax-highlighted and the \x1b guard to skip apply_block_line, rendering raw > instead of the ▌ gutter - Remove fence delimiter preservation from format_response; add \033[0m fallback for plain-text lexer output so pass 2 always skips code content - Update _highlight group numbers to match new 3-group regex (backticks, lang, code) - Keep URL visible in link rendering ([text](url) → underlined text (url)) so users can copy and ctrl+click
Contributor
Author
|
Closing as duplicate — all changes from this PR are included in #4504 ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a full markdown → ANSI rendering engine applied to every non-code line of LLM responses, covering block-level structure and all common inline spans.
Changes
`apply_block_line` — per-line block markdown → ANSI:
`apply_inline_markdown` — inline spans → ANSI:
strikethrough``format_response` extended pipeline:
ANSI corruption fix — `_MD_LINK_RE` uses `(?<!\x1b)` lookbehind: prevents the inline-image reset code `\033[0m` emitted mid-string from being consumed as a link bracket, which would orphan the ESC byte and cause the subsequent ANSI sequence to print as literal text (e.g. `[38;2;…m` visible in terminal output).
Tests
212 passing (`tests/test_display.py` + `tests/test_rich_output.py`).