Skip to content

[Bug] TUI garbles output when final_response_markdown: render — rendered/text priority inverted #16391

@lwj-9650

Description

@lwj-9650

Bug: TUI renders Rich ANSI output as garbled text when final_response_markdown: render

Summary

When config.yaml sets final_response_markdown: render, the TUI gateway sends a payload.rendered field containing Rich-generated ANSI output (cursor movement \x1b[A, line clear \x1b[K, color codes). The Ink-based TUI renderer does not support these complex ANSI sequences, but turnController.ts prioritizes rendered over text, causing garbled output — overlapping white+blue text, misaligned columns, and unreadable responses.

This affects the TUI only. CLI and gateway platforms handle Rich ANSI correctly via their terminal emulators.

Reproduction

  1. Set in config.yaml:
    display:
      final_response_markdown: render
  2. Start hermes --tui
  3. Send any message that triggers a multi-paragraph or table-containing response
  4. Observe garbled output: text overlapping, ANSI escape artifacts, color layering

Root Cause

Two locations in turnController.ts (at commit af3d5150):

1. recordMessageComplete (line 426) — prefers ANSI over raw text:

// Current (official)
const rawText = (payload.rendered ?? payload.text ?? this.bufRef).trimStart()
//                    ^^^^^^^^^ prioritized: Rich ANSI

// Should be for TUI
const rawText = (payload.text ?? payload.rendered ?? this.bufRef).trimStart()
//                    ^^^^ prioritized: raw markdown

2. recordMessageDelta (line 519) — replaces accumulated text with ANSI fragment:

// Current (official)
this.bufRef = rendered ?? this.bufRef + text
//                  ^^^^^^^ when rendered exists, discards all prior bufRef content

// Should be for TUI
this.bufRef = this.bufRef + text
//                  ^^^^^^^^^^^^^ always accumulate raw text, ignore rendered

The delta case is worse: rendered from streaming is an incomplete ANSI fragment (mid-sequence Rich output), which replaces the full accumulated buffer, causing content loss on every delta tick.

Why It Wasn't Caught

Most users either:

  • Don't use TUI, or
  • Don't set final_response_markdown: render

The combination TUI + render is valid config but untested. CLI and Telegram/Discord gateways pass rendered to proper terminal emulators that handle Rich ANSI. Ink's <Md> component cannot.

Evidence

The TUI gateway (tui_gateway/server.py, line 2337-2339) populates payload.rendered via:

rendered = render_message(raw, cols)  # calls agent.rich_output.format_response → Rich Console
if rendered:
    payload["rendered"] = rendered

And for streaming (line 2280-2282):

if streamer and (r := streamer.feed(delta)) is not None:
    payload["rendered"] = r  # incremental Rich ANSI

The render.py bridge always attempts to import agent.rich_output and format via Rich. This is correct for terminal-based UIs but destructive for Ink.

Suggested Fix

Option A (minimal, targeted) — In turnController.ts, swap priority to prefer raw text:

// recordMessageComplete (line 426)
const rawText = (payload.text ?? payload.rendered ?? this.bufRef).trimStart()

// recordMessageDelta (line 519)
this.bufRef = this.bufRef + text  // ignore rendered entirely during streaming

Option B (configurable) — Add a TUI-level config flag that controls rendered/text priority, so render mode can still work for users who want it:

display:
  tui_prefer_raw_text: true  # default: true for Ink safety

Option C (gateway-side) — In tui_gateway/server.py, skip rendered population when rich_output is available, since TUI has its own markdown renderer (markdown.tsx). This avoids sending ANSI to Ink at all.

Environment

  • Hermes Agent: commit af3d5150 (HEAD as of 2026-04-27)
  • Platform: WSL2 (Linux)
  • UI: hermes --tui
  • Config: final_response_markdown: render

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/tuiTerminal UI (ui-tui/ + tui_gateway/)type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions