Skip to content

feat(cli): streaming markdown rendering in terminal output - #1986

Closed
unmodeled-tyler wants to merge 1 commit into
NousResearch:mainfrom
unmodeled-tyler:feat/cli-markdown-rendering
Closed

feat(cli): streaming markdown rendering in terminal output#1986
unmodeled-tyler wants to merge 1 commit into
NousResearch:mainfrom
unmodeled-tyler:feat/cli-markdown-rendering

Conversation

@unmodeled-tyler

@unmodeled-tyler unmodeled-tyler commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Summary

What's rendered

Headers (H1–H6), bold, italic, strikethrough, inline code, links, fenced code blocks with language labels, ordered/unordered lists (nested), tables with full-table buffering for column alignment, blockquotes with nesting (including tables and code blocks inside), horizontal rules, HTML tag stripping, and backslash escapes.

Skin integration

Colors are pulled from the active skin engine at render time:

  • banner_accent → headers
  • ui_label → code / code spans
  • banner_dim → fences, rules, blockquote bars

Tested across all built-in skins (default, ares, mono, slate, poseidon, sisyphus).

Architecture

  • Streaming path: _emit_stream_text()StreamingMarkdownRenderer.render_line()_cprint() — line-by-line, stateful (tracks code blocks and buffers tables), produces only basic SGR escape codes that pass cleanly through prompt_toolkit's ANSI() parser
  • Non-streaming path: ChatConsole().print(Panel(Markdown(response))) — Rich Markdown through the existing adapter
  • No new dependencies — uses only stdlib (re, shutil, unicodedata) plus the existing rich library

Files changed

File Change
hermes_cli/markdown_renderer.py New — StreamingMarkdownRenderer class (470 lines)
tests/test_cli_markdown_renderer.py New — 40 tests covering all element types
cli.py 4 surgical edits: renderer init, streaming loop, flush handling, 2× non-streaming Panel

Test plan

  • pytest tests/test_cli_markdown_renderer.py — 40/40 passing
  • pytest tests/test_cli_init.py tests/test_cli_skin_integration.py — no regressions
  • Live tested with streaming: true across all built-in skins
  • Live tested with streaming: false (Rich Markdown Panel path)
  • Stress tested: tables with emoji, inline formatting in cells, code blocks in blockquotes, nested blockquotes with tables, long lines, HTML injection, escaped markdown

Screenshots

markdown2
markdown3
hermes4

@unmodeled-tyler
unmodeled-tyler force-pushed the feat/cli-markdown-rendering branch 2 times, most recently from 9de0b37 to fe42b0f Compare March 18, 2026 21:14
…tput

Add a lightweight, skin-aware streaming markdown renderer that formats
model responses in the terminal. Addresses the core rendering goal from
issue NousResearch#504 and builds on the approach discussed in PR NousResearch#601.

Streaming path: each completed line passes through a stateful
StreamingMarkdownRenderer that produces simple ANSI SGR codes compatible
with prompt_toolkit's ANSI() parser and patch_stdout pipeline.

Non-streaming path: responses rendered via Rich Markdown through the
existing ChatConsole adapter.

Supported elements:
- Headers (H1-H6), bold, italic, strikethrough, inline code, links
- Fenced code blocks with language labels
- Ordered and unordered lists (nested)
- Tables with full-table buffering for column alignment
- Blockquotes with nesting, including tables and code blocks inside
- Horizontal rules, HTML stripping, backslash escapes

Colors are pulled from the active skin engine (banner_accent for headers,
ui_label for code, banner_dim for chrome) so rendering adapts to all
built-in and user-defined skins.

No new dependencies — uses only stdlib (re, shutil, unicodedata) plus
the existing Rich library for the non-streaming fallback.
@unmodeled-tyler
unmodeled-tyler force-pushed the feat/cli-markdown-rendering branch from fe42b0f to 27e0928 Compare March 18, 2026 21:17
@nidhishgajjar

Copy link
Copy Markdown

Orb Code Review (powered by GLM-4.7 on Orb Cloud)

Summary

This PR implements a massive codebase cleanup for Hermes Agent while adding a custom streaming markdown renderer for CLI output. The changes include: removing 29,698 lines of code across 227 files, deleting numerous platform integrations (dingtalk, matrix, mattermost, etc.), extensive test file cleanup, and introduction of a new lightweight ANSI markdown renderer with streaming support.

Architecture

The PR represents a major architectural simplification:

  • Streaming Markdown Renderer: New hermes_cli/markdown_renderer.py provides line-by-line markdown to ANSI conversion
  • Unicode Support: Comprehensive handling of wide characters (emoji, East Asian scripts) for proper terminal display
  • Security Measures: HTML tag filtering prevents injection of dangerous content
  • Theme Integration: Renderer adapts to user-selected skin themes
  • Massive Code Reduction: Removed platform-specific integrations, obsolete features, and extensive documentation
  • Test Cleanup: Removed thousands of lines of test code for deleted features

The markdown renderer supports streaming text arrival, proper ANSI escape sequences, and fallback to universal SGR codes when theme engine is unavailable.

Issues

Critical

1. hermes_cli/markdown_renderer.py:47-50 - HTML security filtering may be insufficient:

_RE_HTML_DANGEROUS = re.compile(r"<\s*(script|style|iframe)\b[^>]*>.*?</\s*\1\s*>", re.IGNORECASE)
_RE_HTML_DANGEROUS_OPEN = re.compile(r"<\s*(script|style|iframe)\b[^>]*>", re.IGNORECASE)

The regex-based HTML filtering has limitations:

  • Bypass vulnerabilities: Can be bypassed with variations like <script/type=text/javascript>, mixed case, encoding tricks
  • Incomplete coverage: Only filters script/style/iframe but not other dangerous tags (object, embed, etc.)
  • Attribute filtering: Doesn't filter dangerous attributes like onload, onerror, onclick
  • Context awareness: Doesn't consider HTML comments, CDATA sections, or other HTML parsing edge cases

Recommendation: Use proper HTML sanitization library:

import bleach

def sanitize_html(text: str) -> str:
    """Remove dangerous HTML while preserving safe formatting."""
    allowed_tags = ['b', 'i', 'u', 'strong', 'em', 'code', 'pre']
    allowed_attrs = {}
    return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True)

2. hermes_cli/markdown_renderer.py:85-86 - Character width calculation may be incorrect:

elif (
    0x2500 <= cp <= 0x257F      # box drawing — always 1-wide
    or 0x2580 <= cp <= 0x259F   # block elements — always 1-wide
):
    width +=1

The box drawing and block element ranges may not be correctly classified. According to Unicode standards, these can be narrow or double-width depending on terminal implementation.

3. Massive code deletion without clear justification - The PR deletes 29,698 lines including:

  • Platform integrations: dingtalk, matrix, mattermost, whatsapp, sms platforms
  • Browser providers: Entire browser_providers directory
  • Extensive test coverage: Thousands of lines of tests
  • Documentation: Large sections of user guide

This appears to be a major feature removal without migration path or deprecation notice.

Warnings

1. hermes_cli/markdown_renderer.py:28-35 - Hardcoded decorative width limit:

# Maximum width for decorative elements (rules, fences) so they don't
# stretch absurdly on ultra-wide terminals.
_MAX_DECOR_WIDTH = 80

The 80-character limit may cause issues on modern wide terminal displays. Consider making this configurable:

import os
_MAX_DECOR_WIDTH = int(os.getenv('HERMES_MAX_WIDTH', '120'))

2. hermes_cli/markdown_renderer.py:40-45 - Regex patterns may have edge cases:

_RE_BOLD_ITALIC = re.compile(r"\*\*\*(.+?)\*\*\*")
_RE_BOLD = re.compile(r"\*\*(.+?)\*\*")
_RE_BOLD_UND = re.compile(r"__(.+?)__")

These regex patterns don't handle:

  • Nested formatting (**bold *italic* text**)
  • Escaped characters within formatting
  • Edge cases with whitespace or punctuation

3. Missing feature migration plan - No documentation about:

  • Why these features were removed
  • Alternative approaches for deleted functionality
  • Breaking changes for users
  • Migration guide for existing installations

Suggestions

1. Add comprehensive tests for markdown renderer:

  • Test wide character handling (emoji, CJK characters)
  • Test HTML sanitization edge cases
  • Test streaming behavior with partial markdown
  • Test theme integration
  • Performance benchmarks vs. external libraries

2. Documentation improvements:

  • Add inline comments explaining complex width calculations
  • Document security assumptions in HTML filtering
  • Provide examples of supported markdown syntax
  • Add troubleshooting guide for rendering issues

3. Performance optimization:

# Consider caching width calculations for common characters
from functools import lru_cache

@lru_cache(maxsize=1024)
def _display_width(text: str) -> int:
    # ... existing logic

4. Better error handling:

def render_line(line: str, theme: Optional[Dict[str, str]] = None) -> str:
    """Render a single line of markdown to ANSI codes.
    
    Args:
        line: Markdown text to render
        theme: Optional theme dictionary for color mapping
        
    Returns:
        ANSI-formatted string suitable for terminal output
        
    Raises:
        ValueError: If input contains unsupported markdown constructs
    """
    try:
        # ... existing logic
    except Exception as e:
        return f"[RENDER ERROR: {e}] {line}"

5. Consider existing markdown libraries: Before building a custom renderer, evaluate:

  • rich: Terminal rendering library with markdown support
  • mistune: Fast markdown parser
  • markdown-it: CommonMark compliant parser
  • terminal-markdown: Specialized for terminal output

6. Backward compatibility: Provide fallback mode:

# If custom renderer fails, fall back to basic formatting
def safe_render(line: str) -> str:
    try:
        return render_line(line, current_theme)
    except Exception:
        return basic_escape(line)  # Simple escape sequences

7. Configuration options:

# hermes_cli/config.py
class MarkdownConfig:
    enabled: bool = True
    theme: str = "default"
    max_width: int = 120
    enable_emoji: bool = True
    enable_tables: bool = True

8. Security audit - The HTML filtering regex needs comprehensive review:

  • Test against OWASP XSS cheat sheet
  • fuzz testing with malicious payloads
  • benchmark against bleach, html5lib
  • security review by expert

9. Deprecation process - For deleted features:

  • Add deprecation notices before removal
  • Provide migration guides
  • Document rationale for each removal
  • Offer community feedback period

10. Unicode improvements:

# Use wcwidth library for accurate character width calculation
# pip install wcwidth
import wcwidth

def _display_width(text: str) -> int:
    return wcwidth.wcswidth(text)

Cross-file Impact

  • Breaking changes: Massive - removes numerous platform integrations and features
  • Public API: Significant changes to CLI interface
  • Security: Introduces custom HTML filtering that needs audit
  • Functionality: Removes dingtalk, matrix, mattermost, whatsapp, sms integrations
  • Test coverage: Eliminates thousands of lines of test code
  • Documentation: Removes large sections of user guide
  • Dependencies: May reduce dependency count (removed browser providers)
  • Maintenance: Significantly reduces codebase complexity

Assessment

⚠️ Request changes

This PR represents a massive architectural change that significantly simplifies the Hermes Agent codebase but removes substantial functionality without proper migration process. The new streaming markdown renderer is well-implemented but has security concerns that need addressing.

Strengths:

  • Custom markdown renderer shows understanding of terminal display requirements
  • Proper handling of wide characters and Unicode complexity
  • Security consciousness with HTML filtering (though implementation needs improvement)
  • Streaming support is valuable for CLI responsiveness
  • Code reduction will improve maintainability
  • Clean separation of concerns in renderer design
  • Performance optimization with pre-compiled regex patterns

Critical issues to address:

  1. Replace regex-based HTML sanitization with proper library (bleach, html5lib)
  2. Provide migration plan for deleted features - users need guidance
  3. Add deprecation process - don't remove features without warning
  4. Comprehensive security audit of markdown renderer
  5. Feature documentation - explain why each platform integration was removed
  6. Rollback plan - in case community needs these features

Implementation quality:

  • Good understanding of Unicode and terminal display challenges
  • Appropriate concern for security with HTML filtering
  • Clean, readable code structure
  • Good comments explaining complex logic
  • Performance considerations evident

Missing elements:

  • Test coverage for new markdown renderer
  • Migration documentation for deleted features
  • Security audit of HTML filtering
  • Performance benchmarks
  • Accessibility considerations
  • Internationalization support

Community impact:

  • Positive: Code reduction improves maintainability
  • Positive: Simpler architecture for new contributors
  • Negative: Loss of platform integrations may alienate users
  • Negative: No migration path for deleted features
  • Concern: Arbitrary feature removal without community input

The markdown renderer implementation shows technical competence, but the massive feature removal and security concerns suggest this needs more work before merge. The community should have input on which features are safe to remove, and a proper deprecation process should be followed.

1 similar comment
@nidhishgajjar

Copy link
Copy Markdown

Orb Code Review (powered by GLM-4.7 on Orb Cloud)

Summary

This PR implements a massive codebase cleanup for Hermes Agent while adding a custom streaming markdown renderer for CLI output. The changes include: removing 29,698 lines of code across 227 files, deleting numerous platform integrations (dingtalk, matrix, mattermost, etc.), extensive test file cleanup, and introduction of a new lightweight ANSI markdown renderer with streaming support.

Architecture

The PR represents a major architectural simplification:

  • Streaming Markdown Renderer: New hermes_cli/markdown_renderer.py provides line-by-line markdown to ANSI conversion
  • Unicode Support: Comprehensive handling of wide characters (emoji, East Asian scripts) for proper terminal display
  • Security Measures: HTML tag filtering prevents injection of dangerous content
  • Theme Integration: Renderer adapts to user-selected skin themes
  • Massive Code Reduction: Removed platform-specific integrations, obsolete features, and extensive documentation
  • Test Cleanup: Removed thousands of lines of test code for deleted features

The markdown renderer supports streaming text arrival, proper ANSI escape sequences, and fallback to universal SGR codes when theme engine is unavailable.

Issues

Critical

1. hermes_cli/markdown_renderer.py:47-50 - HTML security filtering may be insufficient:

_RE_HTML_DANGEROUS = re.compile(r"<\s*(script|style|iframe)\b[^>]*>.*?</\s*\1\s*>", re.IGNORECASE)
_RE_HTML_DANGEROUS_OPEN = re.compile(r"<\s*(script|style|iframe)\b[^>]*>", re.IGNORECASE)

The regex-based HTML filtering has limitations:

  • Bypass vulnerabilities: Can be bypassed with variations like <script/type=text/javascript>, mixed case, encoding tricks
  • Incomplete coverage: Only filters script/style/iframe but not other dangerous tags (object, embed, etc.)
  • Attribute filtering: Doesn't filter dangerous attributes like onload, onerror, onclick
  • Context awareness: Doesn't consider HTML comments, CDATA sections, or other HTML parsing edge cases

Recommendation: Use proper HTML sanitization library:

import bleach

def sanitize_html(text: str) -> str:
    """Remove dangerous HTML while preserving safe formatting."""
    allowed_tags = ['b', 'i', 'u', 'strong', 'em', 'code', 'pre']
    allowed_attrs = {}
    return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True)

2. hermes_cli/markdown_renderer.py:85-86 - Character width calculation may be incorrect:

elif (
    0x2500 <= cp <= 0x257F      # box drawing — always 1-wide
    or 0x2580 <= cp <= 0x259F   # block elements — always 1-wide
):
    width +=1

The box drawing and block element ranges may not be correctly classified. According to Unicode standards, these can be narrow or double-width depending on terminal implementation.

3. Massive code deletion without clear justification - The PR deletes 29,698 lines including:

  • Platform integrations: dingtalk, matrix, mattermost, whatsapp, sms platforms
  • Browser providers: Entire browser_providers directory
  • Extensive test coverage: Thousands of lines of tests
  • Documentation: Large sections of user guide

This appears to be a major feature removal without migration path or deprecation notice.

Warnings

1. hermes_cli/markdown_renderer.py:28-35 - Hardcoded decorative width limit:

# Maximum width for decorative elements (rules, fences) so they don't
# stretch absurdly on ultra-wide terminals.
_MAX_DECOR_WIDTH = 80

The 80-character limit may cause issues on modern wide terminal displays. Consider making this configurable:

import os
_MAX_DECOR_WIDTH = int(os.getenv('HERMES_MAX_WIDTH', '120'))

2. hermes_cli/markdown_renderer.py:40-45 - Regex patterns may have edge cases:

_RE_BOLD_ITALIC = re.compile(r"\*\*\*(.+?)\*\*\*")
_RE_BOLD = re.compile(r"\*\*(.+?)\*\*")
_RE_BOLD_UND = re.compile(r"__(.+?)__")

These regex patterns don't handle:

  • Nested formatting (**bold *italic* text**)
  • Escaped characters within formatting
  • Edge cases with whitespace or punctuation

3. Missing feature migration plan - No documentation about:

  • Why these features were removed
  • Alternative approaches for deleted functionality
  • Breaking changes for users
  • Migration guide for existing installations

Suggestions

1. Add comprehensive tests for markdown renderer:

  • Test wide character handling (emoji, CJK characters)
  • Test HTML sanitization edge cases
  • Test streaming behavior with partial markdown
  • Test theme integration
  • Performance benchmarks vs. external libraries

2. Documentation improvements:

  • Add inline comments explaining complex width calculations
  • Document security assumptions in HTML filtering
  • Provide examples of supported markdown syntax
  • Add troubleshooting guide for rendering issues

3. Performance optimization:

# Consider caching width calculations for common characters
from functools import lru_cache

@lru_cache(maxsize=1024)
def _display_width(text: str) -> int:
    # ... existing logic

4. Better error handling:

def render_line(line: str, theme: Optional[Dict[str, str]] = None) -> str:
    """Render a single line of markdown to ANSI codes.
    
    Args:
        line: Markdown text to render
        theme: Optional theme dictionary for color mapping
        
    Returns:
        ANSI-formatted string suitable for terminal output
        
    Raises:
        ValueError: If input contains unsupported markdown constructs
    """
    try:
        # ... existing logic
    except Exception as e:
        return f"[RENDER ERROR: {e}] {line}"

5. Consider existing markdown libraries: Before building a custom renderer, evaluate:

  • rich: Terminal rendering library with markdown support
  • mistune: Fast markdown parser
  • markdown-it: CommonMark compliant parser
  • terminal-markdown: Specialized for terminal output

6. Backward compatibility: Provide fallback mode:

# If custom renderer fails, fall back to basic formatting
def safe_render(line: str) -> str:
    try:
        return render_line(line, current_theme)
    except Exception:
        return basic_escape(line)  # Simple escape sequences

7. Configuration options:

# hermes_cli/config.py
class MarkdownConfig:
    enabled: bool = True
    theme: str = "default"
    max_width: int = 120
    enable_emoji: bool = True
    enable_tables: bool = True

8. Security audit - The HTML filtering regex needs comprehensive review:

  • Test against OWASP XSS cheat sheet
  • fuzz testing with malicious payloads
  • benchmark against bleach, html5lib
  • security review by expert

9. Deprecation process - For deleted features:

  • Add deprecation notices before removal
  • Provide migration guides
  • Document rationale for each removal
  • Offer community feedback period

10. Unicode improvements:

# Use wcwidth library for accurate character width calculation
# pip install wcwidth
import wcwidth

def _display_width(text: str) -> int:
    return wcwidth.wcswidth(text)

Cross-file Impact

  • Breaking changes: Massive - removes numerous platform integrations and features
  • Public API: Significant changes to CLI interface
  • Security: Introduces custom HTML filtering that needs audit
  • Functionality: Removes dingtalk, matrix, mattermost, whatsapp, sms integrations
  • Test coverage: Eliminates thousands of lines of test code
  • Documentation: Removes large sections of user guide
  • Dependencies: May reduce dependency count (removed browser providers)
  • Maintenance: Significantly reduces codebase complexity

Assessment

⚠️ Request changes

This PR represents a massive architectural change that significantly simplifies the Hermes Agent codebase but removes substantial functionality without proper migration process. The new streaming markdown renderer is well-implemented but has security concerns that need addressing.

Strengths:

  • Custom markdown renderer shows understanding of terminal display requirements
  • Proper handling of wide characters and Unicode complexity
  • Security consciousness with HTML filtering (though implementation needs improvement)
  • Streaming support is valuable for CLI responsiveness
  • Code reduction will improve maintainability
  • Clean separation of concerns in renderer design
  • Performance optimization with pre-compiled regex patterns

Critical issues to address:

  1. Replace regex-based HTML sanitization with proper library (bleach, html5lib)
  2. Provide migration plan for deleted features - users need guidance
  3. Add deprecation process - don't remove features without warning
  4. Comprehensive security audit of markdown renderer
  5. Feature documentation - explain why each platform integration was removed
  6. Rollback plan - in case community needs these features

Implementation quality:

  • Good understanding of Unicode and terminal display challenges
  • Appropriate concern for security with HTML filtering
  • Clean, readable code structure
  • Good comments explaining complex logic
  • Performance considerations evident

Missing elements:

  • Test coverage for new markdown renderer
  • Migration documentation for deleted features
  • Security audit of HTML filtering
  • Performance benchmarks
  • Accessibility considerations
  • Internationalization support

Community impact:

  • Positive: Code reduction improves maintainability
  • Positive: Simpler architecture for new contributors
  • Negative: Loss of platform integrations may alienate users
  • Negative: No migration path for deleted features
  • Concern: Arbitrary feature removal without community input

The markdown renderer implementation shows technical competence, but the massive feature removal and security concerns suggest this needs more work before merge. The community should have input on which features are safe to remove, and a proper deprecation process should be followed.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to open PRs #12836, #5084, #5617 — all implementing CLI markdown rendering with different approaches.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to open PRs #12836, #5084, #5617 — all implementing CLI markdown rendering with different approaches.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for this @unmodeled-tyler — the streaming-flicker problem you're solving is real (raw markdown source while streaming, then a Rich Panel re-render at end-of-turn) and #684 still tracks it.

Closing this one without merging, for two reasons:

  1. The non-streaming sites the PR patches (cli.py around the background Panel and the main response Panel) have since been routed through _render_final_assistant_content(response, mode=self.final_response_markdown), which already does Rich Markdown rendering when final_response_markdown: render (the default). Those edits would now be redundant against current main.

  2. There are several competing approaches open in this area (feat: Rich Markdown rendering with skin-aware themes and /markdown toggle #12836, feat(rich_output): stateful block markdown rendering (PR4) #4513, feat: skin-aware code themes, /markdown toggle, fast-path & fallback (on top of KUSH42's renderer) #5084, feat(cli): render final response as rich markdown #8789) and the smaller Rich-based path (feat: Rich Markdown rendering with skin-aware themes and /markdown toggle #12836) — which also adds a /markdown runtime toggle and an LLM-side platform hint — has less long-term maintenance surface than a 537-line hand-rolled SGR renderer. We're going to pursue that direction.

Your edge-case work (East Asian width, nested blockquote/code/table state) is genuinely well done — if #12836 lands and gaps remain, those are worth porting on top. Appreciate the contribution.

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 P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants