fix(cli): the glyph layout's reasoning summary never repeats the start of the thought

The "∴ Thought for Ns" line that closes a reasoning block appended the
reasoning's first bold heading, or — with no heading — its first sentence.
Models whose reasoning has no headings (e.g. Claude) therefore got their
opening sentence printed twice: once in the reasoning body streamed above the
line, and again as the summary's title. Drop the title entirely; the reasoning
is already shown in full right above, so the summary is just the duration.

Removes the now-dead _full_title/thought_title/_TITLE_MAX helpers and the
write-only _reasoning_full_text buffer in the stream mixin.
This commit is contained in:
Teknium committed 2026-11-02 07:54:35 -08:00
1 parent 9ef7055f5b
commit aa28a71341
3 files changed
+12 -31

No files matched your search

+5 -23
View File
@@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
_SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
_RST, _BOLD, _DIM = "\x1b[0m", "\x1b[1m", "\x1b[2m"
_RED = "\x1b[38;2;239;83;80m"
_TITLE_MAX = 72
def is_modern(cli) -> bool:
@@ -112,28 +111,11 @@ def answer_label(timestamp: str = "") -> str:
return f"\n{separator(True)}\n{_fg(_skin_hex('ui_accent', '#FFBF00'), bold=True)}{glyph} {name}{_RST}{stamp}"
def _full_title(text: str) -> str:
"""The reasoning's first ``**bold**`` heading, else its first sentence, whitespace-collapsed."""
text = (text or "").strip()
match = re.search(r"\*\*(.+?)\*\*", text)
title = match.group(1) if match else re.split(r"(?<=[.!?])\s|\n", text, maxsplit=1)[0]
return " ".join(title.split()).strip("*# ")
def thought_title(text: str) -> str:
"""Short title for a reasoning block: its first ``**bold**`` heading, else its first sentence."""
title = _full_title(text)
return title if len(title) <= _TITLE_MAX else title[:_TITLE_MAX - 1].rstrip() + "…"
def thought_line(seconds: float | None, text: str) -> str:
"""``∴ Thought for 3s · Planning the change`` (dim italic), closing a reasoning block."""
# The reasoning is printed above this line; a title that IS the whole reasoning would repeat it.
whole = _full_title(text) == " ".join((text or "").split()).strip("*# ")
title = "" if whole else thought_title(text)
def thought_line(seconds: float | None) -> str:
"""``∴ Thought for 3s`` (dim italic), closing a reasoning block. No title: the reasoning is
printed right above, so any excerpt of it would repeat what the user just read."""
took = t("cli.modern.thought_for", seconds=f"{max(seconds or 0.0, 0.0):.0f}") if seconds else t("cli.modern.thought")
body = f"{took} · {title}" if title else took
return f"\x1b[2;3m∴ {body}{_RST}"
return f"\x1b[2;3m∴ {took}{_RST}"
_REASONING_PREVIEW_LINES = 10 # classic's post-hoc reasoning box shows this many unless /reasoning full
@@ -152,7 +134,7 @@ def thought_rows(seconds: float | None, text: str, full: bool) -> list[str]:
rows = ["", separator(False), *(reasoning_text_line(line) for line in shown)]
if len(shown) < len(lines):
rows.append(f"{_DIM} {t('cli.chat.reasoning_more_lines', count=len(lines) - len(shown))}{_RST}")
return [*rows, thought_line(seconds, text), closing_separator(False)]
return [*rows, thought_line(seconds), closing_separator(False)]
def completed_tool_line(cli, function_name: str, args: dict | None, duration: float, result=None,
+3 -5
View File
@@ -273,9 +273,8 @@ class CLIStreamMixin:
from hermes_cli.cli_modern_layout import reasoning_text_line, separator
if not getattr(self, "_reasoning_box_opened", False):
self._reasoning_box_opened, self._reasoning_started_at = True, time.monotonic()
self._reasoning_buf, self._reasoning_full_text = "", ""
self._reasoning_buf = ""
_cprint("\n" + separator(False))
self._reasoning_full_text = getattr(self, "_reasoning_full_text", "") + text
self._reasoning_buf = getattr(self, "_reasoning_buf", "") + text
# Live, as classic: complete lines now, long partial lines force-flushed.
while "\n" in self._reasoning_buf:
@@ -331,10 +330,9 @@ class CLIStreamMixin:
if getattr(self, "_reasoning_buf", ""):
_cprint(reasoning_text_line(self._reasoning_buf))
started = getattr(self, "_reasoning_started_at", 0.0)
_cprint(thought_line(time.monotonic() - started if started else None,
getattr(self, "_reasoning_full_text", "")))
_cprint(thought_line(time.monotonic() - started if started else None))
_cprint(closing_separator(False))
self._reasoning_buf, self._reasoning_full_text, self._reasoning_box_opened = "", "", False
self._reasoning_buf, self._reasoning_box_opened = "", False
if not getattr(self, "_stream_box_live", False):
self._release_held_status_lines()
return
+4 -3
View File
@@ -38,7 +38,8 @@ def test_glyph_layout_scrollback_is_width_independent(monkeypatch, modern):
assert narrow == wide and not boxed, wide
rows = "\n".join(wide).split("\n")
thought = [line for line in rows if line.startswith("∴")]
assert len(thought) == 1 and "Planning the change" in thought[0], rows
# The summary never excerpts the reasoning (printed in full right above it): bare "∴ Thought for Ns".
assert len(thought) == 1 and "·" not in thought[0], rows
# Reasoning streams in full like classic, under a fixed-width dim bar (never re-wraps on resize)
# and closed by the ∴ summary; the answer opens under its own bar.
start = rows.index("**Planning the change**")
@@ -50,8 +51,8 @@ def test_glyph_layout_scrollback_is_width_independent(monkeypatch, modern):
@pytest.mark.parametrize("reasoning", [
"scripted reasoning about gamma zeta-reason",
# Longer than the 72-char title cap: a shortened title would still repeat it.
"scripted reasoning about gamma zeta-reason that keeps going well past the title length cap here",
# A longer multi-sentence block: still never excerpted into the summary.
"scripted reasoning about gamma zeta-reason that keeps going well past any short title length here",
])
def test_one_line_reasoning_is_shown_once(monkeypatch, reasoning):
"""Reasoning streams above its ∴ summary, so a one-sentence block must not reappear as the title."""