Skip to content

feat: Rich Markdown rendering with skin-aware themes and /markdown toggle - #12836

Open
lucaspirola wants to merge 4 commits into
NousResearch:mainfrom
lucaspirola:feat/markdown-pr1-clean
Open

feat: Rich Markdown rendering with skin-aware themes and /markdown toggle#12836
lucaspirola wants to merge 4 commits into
NousResearch:mainfrom
lucaspirola:feat/markdown-pr1-clean

Conversation

@lucaspirola

Copy link
Copy Markdown

Summary

Adds full Rich Markdown rendering for CLI responses using Rich's Markdown class, with skin integration and user control.

  • Rich Markdown rendering — headings, bold, italic, code blocks (Pygments syntax highlighting), tables, lists, blockquotes rendered in the terminal
  • Skin-aware code themes — reads code_theme from the active skin; updates live on /skin change; falls back to monokai
  • Skin-aware text colour — reads banner_text as 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 to display.markdown in config.yaml
  • Fast-path plain-text detection — skips the parser when no markdown syntax is present (regex on first 500 chars)
  • Graceful fallback — all render calls wrapped in try/except; falls back to _RichText.from_ansi()
  • Streaming support — block-boundary detection avoids splitting mid-code-fence; fence state tracked across tokens
  • CLI platform hint — tells the LLM markdown is supported; adds cli_no_markdown variant for when /markdown off is active

Files changed (3)

File Changes
cli.py _render_response(), _emit_stream_markdown(), _find_block_boundary(), _render_markdown_chunk(), _handle_markdown_command(), skin theme init, markdown_enabled flag
hermes_cli/commands.py /markdown + /md command registration
agent/prompt_builder.py CLI and cli_no_markdown platform hint text

Supersedes #5150.

Test plan

  • Headings, bold, italic, code blocks, tables, lists render correctly
  • /markdown off shows raw syntax; /markdown on re-enables
  • /skin ares updates code theme colours live
  • Plain-text responses render unchanged (no parser overhead)
  • Config persists across sessions (display.markdown in config.yaml)

🤖 Generated with Claude Code

lucaspirola and others added 4 commits April 20, 2026 07:02
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>
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Multiple competing PRs for CLI markdown rendering exist (#5084, #5617, #1986) — needs consolidation decision.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Multiple competing PRs for CLI markdown rendering exist (#5084, #5617, #1986) — needs consolidation decision.

@alt-glitch

Copy link
Copy Markdown
Collaborator

Multiple competing PRs exist.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-2801 persists 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 through cli.py:2888-2894.
  • cli.py:3206 selects cli_no_markdown only during agent creation, but /markdown changes only self.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_markdown implementation 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.

Comment thread cli.py
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Comment thread cli.py
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cli.py
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants