fix: streaming display — show tool feed lines and fix response box framing

Two display bugs when streaming is enabled in the interactive CLI:

1. Tool feed lines (┊ 📖 read, ┊ 💻 $, etc.) were invisible during
   streaming sessions. The sequential tool execution path at line 4840
   had a guard `not self._has_stream_consumers()` that skipped the
   entire spinner + cute message display when a stream_delta_callback
   was registered. But no tokens are streaming during tool execution —
   the _executing_tools flag already handles this for _vprint. Removed
   the unnecessary guard.

2. Response box (╭─ ⚕ Hermes ─╮) could wrap intermediate tool-calling
   turns instead of only the final response. Content tokens arrive
   before tool_call tokens during streaming, so the box opens before
   the agent knows tool calls are coming. Now sends a None sentinel
   through the stream callback when tool_calls are confirmed, which
   tells the CLI to close any open box and reset stream state. Only
   the actual final response gets the Hermes border.

Live tested: reasoning blocks → tool feed → reasoning → response box
now display in the correct sequence.
This commit is contained in:
Test 2026-03-20 09:52:47 -07:00
parent d2305cee21
commit 2ef06a04bb
2 changed files with 20 additions and 2 deletions

12
cli.py
View file

@ -1511,10 +1511,22 @@ class HermesCLI:
partial lines and emits complete lines via _cprint to work
reliably with prompt_toolkit's patch_stdout.
A ``None`` sentinel signals that an intermediate turn (with tool
calls) just completed — close any open response/reasoning box
and reset so the final response gets a clean frame.
Reasoning/thinking blocks (<REASONING_SCRATCHPAD>, <think>, etc.)
are suppressed during streaming since they'd display raw XML tags.
The agent strips them from the final response anyway.
"""
if text is None:
# Intermediate turn ended — discard any open box and reset
self._close_reasoning_box()
if self._stream_box_opened:
w = shutil.get_terminal_size().columns
_cprint(f"{_GOLD}╰{'─' * (w - 2)}╯{_RST}")
self._reset_stream_state()
return
if not text:
return

View file

@ -4837,7 +4837,7 @@ class AIAgent:
spinner.stop(cute_msg)
elif self.quiet_mode:
self._vprint(f" {cute_msg}")
elif self.quiet_mode and not self._has_stream_consumers():
elif self.quiet_mode:
face = random.choice(KawaiiSpinner.KAWAII_WAITING)
emoji = _get_tool_emoji(function_name)
preview = _build_tool_preview(function_name, function_args) or function_name
@ -6552,7 +6552,13 @@ class AIAgent:
)
assistant_msg = self._build_assistant_message(assistant_message, finish_reason)
# Signal streaming display that this was an intermediate turn.
# Content may have been streamed (opening the response box) before
# tool_calls were detected. The None sentinel tells the CLI to
# close any open box and reset so the final response gets a clean frame.
self._fire_stream_delta(None)
# If this turn has both content AND tool_calls, capture the content
# as a fallback final response. Common pattern: model delivers its
# answer and calls memory/skill tools as a side-effect in the same