hermes - ✅(Solved) Fix [Bug] TUI garbles output when final_response_markdown: render — rendered/text priority inverted [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16391Fetched 2026-04-28 06:53:40
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×2referenced ×1

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.

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.

Fix Action

Fixed

PR fix notes

PR #16392: fix(tui): prefer raw text over rendered ANSI in TUI message display

Description (problem / solution / changelog)

Summary

Fixes #16391 — TUI garbles output when final_response_markdown: render is active.

When this config key is set, the gateway sends a payload.rendered field containing Rich-generated ANSI output (cursor movement \x1b[A, line clear \x1b[K, colour codes). The Ink-based TUI renderer does not support these complex escape sequences but turnController.ts had rendered taking priority over text in two places:

  • recordMessageComplete (line 426): payload.rendered ?? payload.text ?? this.bufRef → picked ANSI over raw markdown, Ink rendered it as garbled overlapping text.
  • recordMessageDelta (line 519): rendered ?? this.bufRef + text → replaced the accumulated streaming buffer with an incomplete ANSI fragment on every chunk, discarding all prior text.

Changes

  • recordMessageComplete: flipped priority to payload.text ?? payload.rendered ?? this.bufRef so TUI always gets raw markdown.
  • recordMessageDelta: removed the rendered ?? branch; since text is already guard-checked above (line 515), we always accumulate cleanly via this.bufRef + text.

Tests

Two new cases added to createGatewayEventHandler.test.ts:

  • message.complete prefers text over rendered to avoid ANSI garble in TUI
  • message.delta accumulates raw text and ignores rendered ANSI fragments

Both pass; no pre-existing tests broken (7 failures due to missing dist/ink-bundle.js build artifact are pre-existing on main).

Platforms affected

TUI only (hermes --tui). CLI and gateway platforms handle Rich ANSI correctly via their terminal emulators — this change does not affect them since rendered is never injected into those paths.

Changed files

  • ui-tui/src/__tests__/createGatewayEventHandler.test.ts (modified, +27/-0)
  • ui-tui/src/app/turnController.ts (modified, +3/-3)

PR #16464: fix(tui): prefer raw text over rendered ANSI in render-markdown mode

Description (problem / solution / changelog)

What does this PR do?

When display.final_response_markdown: render is set, the TUI gateway populates payload.rendered with Rich-generated ANSI (cursor moves like \x1b[A, line clears \x1b[K, color codes). Ink's <Md> renderer cannot interpret those sequences and displays them as garbled, overlapping output — color layering, misaligned columns, escape artifacts.

The TUI's turnController was prioritizing rendered over the raw markdown text in two places:

  1. recordMessageComplete (ui-tui/src/app/turnController.ts:426) — used payload.rendered ?? payload.text for the final assistant message, so Ink got ANSI instead of raw markdown.
  2. recordMessageDelta (ui-tui/src/app/turnController.ts:519) — this.bufRef = rendered ?? this.bufRef + text. Worse: rendered arrives as an incomplete Rich fragment on every streaming tick, so this replaced the full accumulated buffer with that fragment, dropping prior content.

The CLI and other gateway platforms (Telegram, Discord, …) feed rendered to real terminal emulators that interpret Rich ANSI correctly. Ink does not — it has its own markdown renderer in ui-tui/src/components/markdown.tsx and just needs the raw markdown.

Related Issue

Fixes #16391

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • ui-tui/src/app/turnController.tsrecordMessageComplete now prefers payload.text; recordMessageDelta ignores rendered and accumulates text only.
  • ui-tui/src/__tests__/createGatewayEventHandler.test.ts — two new tests covering the complete and delta paths.

How to Test

  1. Set in ~/.hermes/config.yaml:
    display:
      final_response_markdown: render
  2. Run hermes --tui and ask for a multi-paragraph reply (or one with tables/code blocks).
  3. Before this PR: output is garbled with ANSI escape artifacts.
  4. After this PR: markdown renders cleanly via Ink's <Md> component.

Automated:

cd ui-tui
npm test       # 368 passed (incl. 2 new)
npm run type-check
npx eslint src/app/turnController.ts src/__tests__/createGatewayEventHandler.test.ts

Notes

This is the minimal targeted fix (Option A from the issue). Two follow-ups remain possible but are out of scope here:

  • Option B — make rendered/text priority configurable via display.tui_prefer_raw_text.
  • Option C — gateway-side: skip rendered population for TUI sessions entirely (tui_gateway/server.py:2319 and :2264). That would also save the wasted Rich render work for TUI clients.

Changed files

  • ui-tui/src/__tests__/createGatewayEventHandler.test.ts (modified, +49/-0)
  • ui-tui/src/app/turnController.ts (modified, +11/-3)

Code Example

display:
     final_response_markdown: render

---

// 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

---

// 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

---

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

---

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

---

// 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

---

display:
  tui_prefer_raw_text: true  # default: true for Ink safety
RAW_BUFFERClick to expand / collapse

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

  • #15534 — TUI table border issue (separate, but render mode masks it by never reaching renderTable)
  • #15800 — TUI memory pressure (PR, open)
  • tui_gateway/render.py — the rendering bridge that generates payload.rendered

extent analysis

TL;DR

The most likely fix is to swap the priority of payload.text and payload.rendered in turnController.ts to prefer raw text over Rich ANSI output for the TUI renderer.

Guidance

  • Identify the lines of code in turnController.ts that prioritize rendered over text and adjust them to prefer text for the TUI renderer.
  • Consider adding a configurable flag to control the priority of rendered vs text for the TUI, allowing users to choose their preferred behavior.
  • Review the tui_gateway/server.py code to determine if skipping rendered population when rich_output is available is a viable solution.
  • Test the changes with different configurations and inputs to ensure the fix works as expected.

Example

// 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

Notes

The provided fix options (A, B, C) offer different approaches to addressing the issue. Option A is the most straightforward, while Option B provides more flexibility. Option C requires changes to the tui_gateway/server.py code.

Recommendation

Apply workaround by swapping the priority of payload.text and payload.rendered in turnController.ts to prefer raw text over Rich ANSI output for the TUI renderer, as this is the most direct and targeted solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug] TUI garbles output when final_response_markdown: render — rendered/text priority inverted [2 pull requests, 1 participants]