feat: Rich Markdown rendering with skin-aware themes and /markdown toggle - #12836
feat: Rich Markdown rendering with skin-aware themes and /markdown toggle#12836lucaspirola wants to merge 4 commits into
Conversation
The CLI was displaying raw markdown syntax (**bold**, ```code```, etc.) because responses were rendered through _rich_text_from_ansi() which only interprets ANSI escapes. This adds full markdown rendering using Rich's built-in Markdown class with Pygments syntax highlighting. Changes: - Update CLI platform hint to encourage LLM markdown generation - Add _render_response() helper with fast-path regex detection - Render non-streaming responses via Rich Markdown in Panel - Add block-boundary streaming: accumulate tokens, render complete blocks through Rich Markdown, buffer incomplete trailing block - Add /markdown (alias /md) command to toggle on/off with persistence - Conditionally omit Panel style= when markdown enabled to preserve heading/code/bold colors from being washed out Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make the markdown renderer adapt to the active skin's colour palette
instead of hardcoding monokai/white. _render_response() now accepts
code_theme and text_color from the skin — banner_text becomes the base
paragraph colour (Rich Markdown's style= layers underneath element
styles, preserving heading/code/bold colours), and code_theme falls
back to monokai unless a skin overrides it via get_color("code_theme").
Zero changes to skin definitions or SkinConfig — existing skins and
user-defined skins work automatically through get_color() fallbacks.
Also adds docstrings and inline comments to all markdown rendering code
(regex, fast-path, block boundary detection, chunk rendering, streaming
strategy, flush, command handler) for clarity and maintainability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CLI now has a full Rich Markdown renderer, so the platform hint should tell the LLM to use markdown instead of discouraging it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g, term width cache - Raise fast-path scan window from 500 to 8192 chars so agentic responses with plain-text preambles before tables/code blocks still get markdown rendering - Add cli_no_markdown platform hint; pass it when markdown_enabled=False so the LLM doesn't emit markdown that would display as raw syntax - Add logger.debug() to both renderer exception handlers so render failures leave a diagnostic trail - Cache terminal width at stream-open time to avoid a syscall per chunk - Clarify in-code comment: skin theme is captured per response, not mid-stream (/skin takes effect on the next response) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 similar comment
|
Multiple competing PRs exist. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the substantial CLI rendering work. Current main already has a tested Rich Markdown render mode via display.final_response_markdown (cli.py:2467-2502, tests/cli/test_cli_markdown_rendering.py:15-193), so this needs to be consolidated with that path rather than restoring the older renderer.
Problems
cli.py:2787-2801persists fence state across complete rescans of the retained buffer. On the next token delta, the same unmatched opening fence toggles that state again; a blank line inside an unfinished fence can then be emitted as a completed block throughcli.py:2888-2894.cli.py:3206selectscli_no_markdownonly during agent creation, but/markdownchanges onlyself.markdown_enabled(cli.py:6639-6644). Existing agents are reused, so the promised prompt change does not take effect in an already-started session.- No tests accompany the new renderer, streaming state machine, command, or prompt behavior.
Suggested changes
- Re-scope onto current main's
_render_final_assistant_content()/display.final_response_markdownimplementation and preserve its ANSI, Windows-path, and table handling. - Rework fence detection to avoid rescanning retained text with accumulated state, then add token-boundary tests for incomplete backtick and tilde fences.
- Keep any prompt-policy change deferred to a new session so the active conversation's cached system prompt remains stable.
Automated hermes-sweeper review.
| in half — the block is held in buffer until the closing fence | ||
| arrives, then rendered in one piece with syntax highlighting. | ||
| """ | ||
| in_fence = self._stream_md_fence_open |
There was a problem hiding this comment.
_stream_md_buf is retained and this function rescans it on every delta, but _stream_md_fence_open already reflects the previous full scan. An unmatched opening fence is therefore toggled twice on the next call, so a blank line inside an unfinished fenced block can become a false safe boundary. Derive fence state from the retained buffer on each scan, or scan only new input with a correctly maintained state.
| platform="cli", | ||
| # Use a markdown-off platform hint when rendering is disabled | ||
| # so the LLM doesn't produce markdown that would display raw. | ||
| platform="cli" if self.markdown_enabled else "cli_no_markdown", |
There was a problem hiding this comment.
This prompt variant is selected only when AIAgent is created. /markdown off updates self.markdown_enabled but does not replace the existing agent, so the claimed no-Markdown instruction does not take effect after the first turn. Do not invalidate an active session's prompt cache; defer prompt-policy changes to a new session or keep this toggle display-only.
| try: | ||
| # Use skin text colour as base style; "none" means default terminal colour | ||
| md_style = text_color if text_color else "none" | ||
| return _RichMarkdown(text, code_theme=code_theme, style=md_style) |
There was a problem hiding this comment.
This Markdown path passes the original text directly to Rich, unlike the current renderer, which normalizes ANSI through _rich_text_from_ansi(...).plain before constructing Markdown. Preserve that normalization and add an ANSI-bearing Markdown test.
Summary
Adds full Rich Markdown rendering for CLI responses using Rich's
Markdownclass, with skin integration and user control.code_themefrom the active skin; updates live on/skinchange; falls back to monokaibanner_textas base paragraph colour so prose matches the theme while headings/code/bold keep their element styles/markdown [on|off]command (alias/md) — toggle at runtime, persists todisplay.markdownin config.yamltry/except; falls back to_RichText.from_ansi()cli_no_markdownvariant for when/markdown offis activeFiles changed (3)
cli.py_render_response(),_emit_stream_markdown(),_find_block_boundary(),_render_markdown_chunk(),_handle_markdown_command(), skin theme init,markdown_enabledflaghermes_cli/commands.py/markdown+/mdcommand registrationagent/prompt_builder.pySupersedes #5150.
Test plan
/markdown offshows raw syntax;/markdown onre-enables/skin aresupdates code theme colours livedisplay.markdownin config.yaml)🤖 Generated with Claude Code