mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-22 05:22:09 +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
|
|
@ -156,6 +156,108 @@ def test_passes_non_table_lines_through_around_a_table():
|
|||
assert all(o == offsets[0] for o in offsets)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vertical fallback for tables wider than the terminal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_overflow_falls_back_to_vertical_when_table_wider_than_terminal():
|
||||
"""A horizontal table that would exceed the available width must
|
||||
drop to vertical key-value rendering so the terminal does not
|
||||
soft-wrap mid-cell (which destroys column alignment visually)."""
|
||||
|
||||
src = dedent(
|
||||
"""\
|
||||
| Item | Description | Notes |
|
||||
|------|-------------|-------|
|
||||
| a | short | ok |
|
||||
| b | this is a much longer description that stretches the column wider than the others by a lot | fine |
|
||||
| c | tiny | - |
|
||||
"""
|
||||
)
|
||||
|
||||
out = realign_markdown_tables(src, available_width=100)
|
||||
|
||||
# No horizontal pipe-bordered rows: vertical mode emits "Header: value"
|
||||
# lines and a ─ separator instead.
|
||||
assert "|" not in out
|
||||
assert "Item: a" in out
|
||||
assert "Description: short" in out
|
||||
assert "Notes: ok" in out
|
||||
# Body rows separated by ─ rule
|
||||
assert "──" in out
|
||||
|
||||
# Every emitted line fits the available width.
|
||||
for line in out.split("\n"):
|
||||
assert wcswidth(line) <= 100, f"line wider than budget: {line!r}"
|
||||
|
||||
|
||||
def test_horizontal_kept_when_table_fits():
|
||||
"""A table that fits the terminal must keep the horizontal
|
||||
pipe-bordered rendering — vertical fallback only kicks in when
|
||||
soft-wrap is unavoidable."""
|
||||
|
||||
src = dedent(
|
||||
"""\
|
||||
| Name | Age |
|
||||
|------|-----|
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |
|
||||
"""
|
||||
)
|
||||
|
||||
out = realign_markdown_tables(src, available_width=100)
|
||||
|
||||
# Pipe-bordered rendering survives.
|
||||
body_rows = [ln for ln in out.split("\n") if ln.strip().startswith("|")]
|
||||
assert len(body_rows) == 4
|
||||
offsets = [_column_offsets(r) for r in body_rows]
|
||||
assert all(o == offsets[0] for o in offsets)
|
||||
|
||||
|
||||
def test_vertical_fallback_wraps_long_cell_text_with_indent():
|
||||
src = dedent(
|
||||
"""\
|
||||
| Key | Value |
|
||||
|-----|-------|
|
||||
| x | this value is long enough that wrapping the value to fit a narrow terminal width is required even in vertical mode |
|
||||
"""
|
||||
)
|
||||
|
||||
out = realign_markdown_tables(src, available_width=60)
|
||||
|
||||
lines = out.split("\n")
|
||||
assert lines[0].startswith("Key: x")
|
||||
# First "Value:" line + at least one continuation indented by 2 spaces.
|
||||
value_idx = next(i for i, l in enumerate(lines) if l.startswith("Value:"))
|
||||
assert lines[value_idx + 1].startswith(" ")
|
||||
# Every line still fits the budget.
|
||||
for line in lines:
|
||||
assert wcswidth(line) <= 60
|
||||
|
||||
|
||||
def test_overflow_falls_back_to_vertical_for_cjk_too():
|
||||
"""CJK content can also push a table over the terminal budget;
|
||||
the vertical fallback should kick in regardless of script."""
|
||||
|
||||
src = dedent(
|
||||
"""\
|
||||
| 模型 | 描述 | 备注 |
|
||||
|------|------|------|
|
||||
| 千问 | 一个相当长的描述用于把列宽撑得超过可用终端宽度从而触发竖排回退 | 通过 |
|
||||
| 文心 | 短 | × |
|
||||
"""
|
||||
)
|
||||
|
||||
out = realign_markdown_tables(src, available_width=50)
|
||||
|
||||
assert "|" not in out
|
||||
assert "模型: 千问" in out
|
||||
assert "模型: 文心" in out
|
||||
for line in out.split("\n"):
|
||||
assert wcswidth(line) <= 50, f"line wider than budget: {line!r}"
|
||||
|
||||
|
||||
def test_handles_ragged_rows_by_padding_short_rows():
|
||||
src = dedent(
|
||||
"""\
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue