mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-20 05:01:30 +00:00
fix(cli): vertical fallback for markdown tables wider than terminal (#23948)
Follow-up to #23863 (CJK table alignment). The realigner was correctly padding pipes to identical column offsets, but when a table's natural width exceeds terminal cells it produced lines that the terminal soft-wrapped mid-cell, destroying column alignment visually even though the bytes were perfectly padded. Reported as 'columns are not aligned' on tables containing one long row alongside several short rows. Approach mirrors Claude Code's MarkdownTable.tsx narrow-terminal fallback: when realign_markdown_tables is given an available_width budget and the rebuilt horizontal table exceeds it, render each body row as 'Header: value' lines separated by a thin ─ rule. Word-wraps oversize values at the budget with a 2-space continuation indent. - agent/markdown_tables.py: realign_markdown_tables(text, available_width=None); threshold check at the top of _render_block flips into a new _render_vertical fallback. Includes _wrap_to_width with hard-break for tokens longer than the budget. - cli.py: helper _terminal_width_for_streaming() returns shutil.get_terminal_size().columns minus _STREAM_PAD and a 2-cell safety margin; passed to all three realign call sites (_render_final_assistant_content for strip+render Panel paths, and the streaming flushers in _emit_stream_text / _flush_stream). - tests/agent/test_markdown_tables.py: 4 new tests covering the overflow-vertical fallback for ASCII + CJK content, the 'fits → keep horizontal' case, and the long-cell wrap with indent. Live-verified: with COLUMNS=100, the user's reported 'long row in ASCII table' case now renders as vertical key-value rows that all fit the panel; the 6-column CJK comparison table still renders as an aligned horizontal table because it fits inside 100 cols.
This commit is contained in:
parent
825bd50e6b
commit
ea1d0462cf
3 changed files with 281 additions and 8 deletions
40
cli.py
40
cli.py
|
|
@ -1354,16 +1354,48 @@ def _preserve_windows_dot_segments_for_markdown(text: str) -> str:
|
|||
return _WINDOWS_PATH_WITH_DOT_SEGMENT_RE.sub(_protect, text)
|
||||
|
||||
|
||||
def _terminal_width_for_streaming() -> int:
|
||||
"""Display cells available inside the streamed response box.
|
||||
|
||||
The streaming path indents every line by ``_STREAM_PAD`` (4 cells)
|
||||
inside an open response panel. The realigner uses this number as
|
||||
its budget when deciding whether to keep a horizontal table or
|
||||
fall back to vertical key-value rendering. We subtract a small
|
||||
safety margin so terminal-resize races don't push a borderline
|
||||
table into mid-cell soft-wrap.
|
||||
"""
|
||||
|
||||
try:
|
||||
cols = shutil.get_terminal_size((80, 24)).columns
|
||||
except Exception:
|
||||
cols = 80
|
||||
return max(20, cols - len(_STREAM_PAD) - 2)
|
||||
|
||||
|
||||
def _render_final_assistant_content(text: str, mode: str = "render"):
|
||||
"""Render final assistant content as markdown, stripped text, or raw text."""
|
||||
from rich.markdown import Markdown
|
||||
|
||||
# Estimate the cells available to the rendered table. The Panel
|
||||
# used by the background-task / final-response path has 4 cells of
|
||||
# left+right padding plus 1 cell of border on each side, plus the
|
||||
# _STREAM_PAD indent that streamed content uses. Subtract a small
|
||||
# safety margin so resize races don't push a borderline table into
|
||||
# soft-wrap.
|
||||
try:
|
||||
cols = shutil.get_terminal_size((80, 24)).columns
|
||||
except Exception:
|
||||
cols = 80
|
||||
panel_width = max(20, cols - 12)
|
||||
|
||||
normalized_mode = str(mode or "render").strip().lower()
|
||||
if normalized_mode == "strip":
|
||||
# Strip first — inline markdown inside cells (`code`, **bold**, ~~strike~~)
|
||||
# changes cell display width — then re-align so the column padding
|
||||
# reflects the final visible text, not the marker-decorated source.
|
||||
return _RichText(realign_markdown_tables(_strip_markdown_syntax(text)))
|
||||
return _RichText(
|
||||
realign_markdown_tables(_strip_markdown_syntax(text), panel_width)
|
||||
)
|
||||
if normalized_mode == "raw":
|
||||
return _rich_text_from_ansi(text or "")
|
||||
|
||||
|
|
@ -1374,7 +1406,7 @@ def _render_final_assistant_content(text: str, mode: str = "render"):
|
|||
# (narrow panels, etc.) at least see consistent input.
|
||||
plain = _rich_text_from_ansi(text or "").plain
|
||||
plain = _preserve_windows_dot_segments_for_markdown(plain)
|
||||
plain = realign_markdown_tables(plain)
|
||||
plain = realign_markdown_tables(plain, panel_width)
|
||||
return Markdown(plain)
|
||||
|
||||
|
||||
|
|
@ -3662,7 +3694,7 @@ class HermesCLI:
|
|||
joined = "\n".join(buf)
|
||||
if self.final_response_markdown == "strip":
|
||||
joined = _strip_markdown_syntax(joined)
|
||||
block = realign_markdown_tables(joined)
|
||||
block = realign_markdown_tables(joined, _terminal_width_for_streaming())
|
||||
for ln in block.split("\n"):
|
||||
_emit_one(ln)
|
||||
|
||||
|
|
@ -3726,7 +3758,7 @@ class HermesCLI:
|
|||
self._in_stream_table = False
|
||||
if self.final_response_markdown == "strip":
|
||||
joined = _strip_markdown_syntax(joined)
|
||||
block = realign_markdown_tables(joined)
|
||||
block = realign_markdown_tables(joined, _terminal_width_for_streaming())
|
||||
for ln in block.split("\n"):
|
||||
_cprint(f"{_STREAM_PAD}{_tc}{ln}{_RST}" if _tc else f"{_STREAM_PAD}{ln}")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue