feat(cli): restore live per-tool elapsed timer in TUI spinner (#7359)

Brings back the live elapsed time counter that was lost when the CLI
transitioned from raw KawaiiSpinner animation to prompt_toolkit TUI.

The original implementation (Feb 2026) used KawaiiSpinner per tool call
with \r-based animation showing '(4.2s)' ticking up live. When
patch_stdout was introduced, the \r animation was disabled and replaced
with a static _spinner_text widget that only showed the tool name.

Now the spinner widget shows elapsed time again:
  💻 git log --oneline  (3.2s)

Implementation:
- Track _tool_start_time (monotonic) on tool.started events
- Clear it on tool.completed and thinking transitions
- get_spinner_text() computes live elapsed on each TUI repaint
- The existing poll loop already invalidates every ~0.15s, so no
  extra timer thread is needed

Addresses #4287.
This commit is contained in:
Teknium 2026-04-10 13:09:41 -07:00 committed by GitHub
parent 0bea603510
commit f83e86d826
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

26
cli.py
View file

@ -1719,6 +1719,7 @@ class HermesCLI:
self._secret_state = None
self._secret_deadline = 0
self._spinner_text: str = "" # thinking spinner text for TUI
self._tool_start_time: float = 0.0 # monotonic timestamp when current tool started (for live elapsed)
self._command_running = False
self._command_status = ""
self._attached_images: list[Path] = []
@ -2130,6 +2131,7 @@ class HermesCLI:
if not text:
self._flush_reasoning_preview(force=True)
self._spinner_text = text or ""
self._tool_start_time = 0.0 # clear tool timer when switching to thinking
self._invalidate()
# ── Streaming display ────────────────────────────────────────────────
@ -6145,11 +6147,20 @@ class HermesCLI:
Updates the TUI spinner widget so the user can see what the agent
is doing during tool execution (fills the gap between thinking
spinner and next response). Also plays audio cue in voice mode.
On tool.started, records a monotonic timestamp so get_spinner_text()
can show a live elapsed timer (the TUI poll loop already invalidates
every ~0.15s, so the counter updates automatically).
"""
# Only act on tool.started; ignore tool.completed, reasoning.available, etc.
if event_type == "tool.completed":
import time as _time
self._tool_start_time = 0.0
self._invalidate()
return
if event_type != "tool.started":
return
if function_name and not function_name.startswith("_"):
import time as _time
from agent.display import get_tool_emoji
emoji = get_tool_emoji(function_name)
label = preview or function_name
@ -6158,6 +6169,7 @@ class HermesCLI:
if _pl > 0 and len(label) > _pl:
label = label[:_pl - 3] + "..."
self._spinner_text = f"{emoji} {label}"
self._tool_start_time = _time.monotonic()
self._invalidate()
if not self._voice_mode:
@ -8359,6 +8371,17 @@ class HermesCLI:
txt = cli_ref._spinner_text
if not txt:
return []
# Append live elapsed timer when a tool is running
t0 = cli_ref._tool_start_time
if t0 > 0:
import time as _time
elapsed = _time.monotonic() - t0
if elapsed >= 60:
_m, _s = int(elapsed // 60), int(elapsed % 60)
elapsed_str = f"{_m}m {_s}s"
else:
elapsed_str = f"{elapsed:.1f}s"
return [('class:hint', f' {txt} ({elapsed_str})')]
return [('class:hint', f' {txt}')]
def get_spinner_height():
@ -8893,6 +8916,7 @@ class HermesCLI:
finally:
self._agent_running = False
self._spinner_text = ""
self._tool_start_time = 0.0
app.invalidate() # Refresh status line