feat: Rich-based rendering engine with intra-line diff highlighting (PR1) - #4470
feat: Rich-based rendering engine with intra-line diff highlighting (PR1)#4470KUSH42 wants to merge 13 commits into
Conversation
2aff5d2 to
80dab43
Compare
00e8cf7 to
4f42b5b
Compare
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.
Adds _DIFF_MAX_LINES = 80 and a max_lines parameter to to_lines(). Outputs beyond the cap get a dim footer matching _highlight_block's style. Passes max_lines=0 in _render_inline_unified_diff so the section-level budget in _summarize_rendered_diff_sections is unaffected.
Apply Pygments token colours to diff line content so that keywords, strings, comments, numbers etc. are visually distinct within the red/green diff bands — matching the style shown in tools like delta. Changes: * Two new colour constants — _DIFF_BG_ADD_HL / _DIFF_BG_DEL_HL — a noticeably brighter shade of the base diff background, used to mark changed character ranges in intra-line diffs without touching the foreground (no conflict with syntax token colours). * _syntax_text(content, filename) — new helper that calls SyntaxHighlighter.to_markup(), converts to a Rich Text (foreground colours only), and strips the trailing newline Pygments always appends so line lengths stay accurate. * _flat_add / _flat_del — now accept an optional filename hint and apply syntax highlighting via _syntax_text, then overlay the base diff background with Text.stylize(). * _intra_diff — redesigned: syntax-highlight both lines first, apply the base diff background across the whole text, then apply the brighter highlight background (bold) only over changed character ranges via a second stylize() pass. Foreground colours come entirely from syntax; background colours entirely from diff state. * _style() — passes filename (explicit_filename or from_path) through to all three helpers above, and syntax-highlights context lines (dim style overlaid on syntax colours). * Tests updated to match the new interface: _intra_diff now returns ([Text], [Text]) with spans rather than lists of single-span segments; changed-region detection checks for bright bg spans instead of bright foreground colour names.
Line numbers and the -/+ prefix had no background set, so they rendered on the terminal default (black) while the content immediately to their right had the dark-red/dark-green diff background — creating a jarring visual break. Extend bgcolor to the line-number and sigil Text objects so the entire deleted/added row is uniformly on the diff background, matching the visual style of delta, GitHub, and VS Code's diff view.
a7ccaa0 to
74eb7ef
Compare
_MAX_INLINE_DIFF_LINES (80), _MAX_INLINE_DIFF_FILES (6), and _PREVIEW_MAX_LINES (40) were hardcoded in agent/display.py with no user-facing knob. Wire them to config.yaml under display: diff_max_lines: 80 # lines shown per inline diff before "… omitted" summary diff_max_files: 6 # file sections shown per inline diff preview_max_lines: 40 # lines shown in read_file/execute_code/terminal previews Adds set_diff_limits() and set_preview_max_lines() setters in display.py; cli.py reads and applies all three at init alongside code_highlight.
Two bugs in _current_reasoning_callback(): 1. show_reasoning=True + streaming=False returned None — non-streaming mode got no live reasoning callback even when user enabled it (post-turn box still worked, but no intermediate display during tool-call loops) 2. verbose=True + show_reasoning=False returned _on_reasoning — verbose mode leaked reasoning into the display regardless of the user's explicit setting Fix: show_reasoning is the sole gate. When on, pick callback by streaming mode (stream_reasoning_delta vs on_reasoning). Verbose no longer overrides.
…y's fixes - test_reasoning_command.py: install prompt_toolkit/fire stubs at module level (conditional on missing) so all 58 tests can import from cli without patching individually; fixes 38 pre-existing failures. Correct two wrong assertions in TestReasoningDisplayModeSelection that reflected old buggy behaviour (show_reasoning=True+non-streaming returned None; verbose leaked callback). Add test_show_reasoning_off_returns_none and test_verbose_without_show_reasoning_returns_none. - test_display.py: import set_diff_limits/set_preview_max_lines; add TestDisplayLimitSetters covering global mutation and truncation behaviour.
35722d2 to
ea4b477
Compare
spinner_loop only fast-refreshed (0.1 s) during _command_running, leaving _agent_running on the 1 s idle cadence — so the status-bar prompt showed a static ⚕ and tool-progress updates in the spinner widget lagged by up to a second. Add _agent_running to the fast-refresh branch alongside _command_running, and replace the static ⚕ in _get_tui_prompt_fragments with _command_spinner_frame() so the braille dots animate while the agent is working, matching the slash-command experience.
Spinner style - Add _SPINNER_STYLES dict (dots, bounce, grow, arrows, star, moon, pulse, clock, none) so users can pick via display.spinner_style in config.yaml; default remains "dots" (braille). - At HermesCLI init, read spinner_style and override the module-level _COMMAND_SPINNER_FRAMES global so _command_spinner_frame() picks the right sequence for both the status-bar prompt and the tab title. Terminal tab / window title - Add display.title_spinner (default true) and display.title_base (default "Hermes") config keys. - spinner_loop emits OSC 0 (\033]0;…\007) via app.output.write_raw() at each 0.1 s tick while active, giving a live "⠋ Hermes" → "⠙ Hermes" animation in the terminal tab; resets to the bare base string on idle. Errors are silenced so terminals that don't support OSC 0 aren't affected.
…hlight active - Import LanguageDetector into display.py and instantiate as _rich_detector; render_read_file_preview and _extract_file_language_from_command were calling _rich_detector.detect_from_filename but the name was never bound (SyntaxHighlighter has no detect_from_filename method). - Suppress execute_code first-line snippet in get_cute_tool_message when _code_highlight_active is True — the highlighted block renders immediately after, so printing the snippet again is redundant duplication. - Add missing json, _highlight_block, _result_succeeded, get_cute_tool_message imports to test_display.py so the test suite can actually run.
8e7b587 to
1b2708e
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the detailed renderer and focused unit coverage. The line-numbered/intra-line diff idea is still absent from current main, but a salvage needs adjustment for current surfaces.
Problems
agent/display.py:451emits ANSI lines fromDiffRenderer, but currentui-tui/src/app/createGatewayEventHandler.ts:740appliesstripAnsitoinline_diff. The Rich backgrounds and intra-line emphasis therefore disappear in the TUI, which consumes the same edit-preview callback path.- The PR also changes unrelated security-sensitive redaction at
agent/redact.py:135, plus file-read, reasoning, and spinner behavior. Current main has later redaction work (fdb9620ac,c1c179a23,a57306654) that a renderer salvage must retain.
Suggested changes
- Carry structured diff styling to the TUI, or render the diff natively there, and add a
tool.complete.inline_diffintegration test. - Split the unrelated changes before salvaging the renderer.
Automated hermes-sweeper review.
| """ | ||
| if _RICH_OUTPUT: | ||
| try: | ||
| return _rich_diff.to_lines(diff, max_lines=0) |
There was a problem hiding this comment.
This returns ANSI-only output. Current ui-tui/src/app/createGatewayEventHandler.ts:740 strips ANSI from inline_diff, so the proposed backgrounds and intra-line emphasis do not reach the TUI. Please carry structured styling through the event or add a TUI-native renderer and integration coverage.
| return text | ||
| if not _REDACT_ENABLED: | ||
| return text | ||
| # Fast path for large plain text blobs with no secret-like markers. |
There was a problem hiding this comment.
This security-sensitive redaction optimization is unrelated to the Rich diff feature. Please split it from this renderer PR; current main has later redaction fixes that should be preserved independently during salvage.
Introduces
agent/rich_output.py— a self-contained Rich/Pygments rendering toolkit with no project-specific imports — and upgrades the write-action diff previews added in #4423 with line numbers, per-file summary headers, and intra-line character-level highlighting.Current diff vs new diff (includes changes introduced with PR2-5)

Standalone




Changes
agent/rich_output.py(new module)Core rendering toolkit — consumed here and by the follow-up PRs:
LanguageDetector— extension map + content-pattern heuristicsSyntaxHighlighter— Pygments → Rich markup → ANSI stringFilePathFormatter— per-filetype icons, compact relative pathsDiffRenderer— unified diff → RichTextobjects with line numbers_intra_diff— character-level segment lists viaSequenceMatcher(exposed for testing)_parse_diff_filename— stripsa//b/prefixes, handles/dev/null(exposed for testing)clean_command_output— strips venv/stacktrace noise from command outputagent/display.py(modifications)DiffRendererandSyntaxHighlighterfromrich_outputat module load; sets_RICH_OUTPUT = True/Falseso the rest ofdisplay.pydegrades gracefully ifrich/pygmentsare absent_render_inline_unified_diff— enhanced: triesDiffRenderer.to_lines()first, falls back to the feat: add inline diff previews for write actions #4423 ANSI path on any exceptionhighlight_code()— new public function wrappingSyntaxHighlighter.to_ansi(); used by the follow-up syntax highlighting PRWhat
DiffRendereradds over the #4423 ANSI path● filename.py Added N lines, removed M linesabove each file section0.5— below it flat highlighting is used instead-/+runs zipped for intra-line comparison, avoiding cross-hunk false pairingsshutil.get_terminal_size; truncation delegated to the existing_summarize_rendered_diff_sectionswrapper (max_lines=0in the Rich path)Tests
51 passing in
tests/test_rich_output.py.tests/test_display.pyassertions updated for the new header format.