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
- Set in
config.yaml:
display:
final_response_markdown: render
- Start
hermes --tui
- Send any message that triggers a multi-paragraph or table-containing response
- 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
Bug: TUI renders Rich ANSI output as garbled text when
final_response_markdown: renderSummary
When
config.yamlsetsfinal_response_markdown: render, the TUI gateway sends apayload.renderedfield 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, butturnController.tsprioritizesrenderedovertext, 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
config.yaml:hermes --tuiRoot Cause
Two locations in
turnController.ts(at commitaf3d5150):1.
recordMessageComplete(line 426) — prefers ANSI over raw text:2.
recordMessageDelta(line 519) — replaces accumulated text with ANSI fragment:The delta case is worse:
renderedfrom 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:
final_response_markdown: renderThe combination
TUI + renderis valid config but untested. CLI and Telegram/Discord gateways passrenderedto proper terminal emulators that handle Rich ANSI. Ink's<Md>component cannot.Evidence
The TUI gateway (
tui_gateway/server.py, line 2337-2339) populatespayload.renderedvia:And for streaming (line 2280-2282):
The
render.pybridge always attempts to importagent.rich_outputand 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:Option B (configurable) — Add a TUI-level config flag that controls rendered/text priority, so
rendermode can still work for users who want it:Option C (gateway-side) — In
tui_gateway/server.py, skiprenderedpopulation whenrich_outputis available, since TUI has its own markdown renderer (markdown.tsx). This avoids sending ANSI to Ink at all.Environment
af3d5150(HEAD as of 2026-04-27)hermes --tuifinal_response_markdown: renderRelated
rendermode masks it by never reachingrenderTable)tui_gateway/render.py— the rendering bridge that generatespayload.rendered