fix(cli): suppress token placeholder on first invoke (#3302)

The status bar footer was showing "... tokens" immediately on the first
LLM invocation, before any real token count had been established. This
created visual noise during the initial streaming period when no context
tokens exist yet.
This commit is contained in:
Mason Daugherty
2026-05-10 23:16:11 -04:00
committed by GitHub
parent aa28ff7376
commit c21dee2e9b
4 changed files with 19 additions and 7 deletions
@@ -384,6 +384,8 @@ class StatusBar(Horizontal):
(The actual context is larger because the generation was interrupted before
the model reported final usage.)
"""
_has_token_count: bool = False
"""Whether the status bar has displayed a real token count this session."""
def watch_tokens(self, new_value: int) -> None:
"""Update token display when count changes."""
@@ -425,6 +427,7 @@ class StatusBar(Horizontal):
approximate: Append "+" to indicate the count is stale.
"""
self._approximate = approximate
self._has_token_count = count > 0
if self.tokens == count:
# Reactive dedup would skip the watcher — call render directly.
self._render_tokens(count, approximate=approximate)
@@ -435,6 +438,8 @@ class StatusBar(Horizontal):
def show_pending_tokens(self) -> None:
"""Show an unknown token count placeholder during streaming."""
if not self._has_token_count:
return
try:
self.query_one("#tokens-display", Static).update("... tokens")
except NoMatches:
@@ -145,7 +145,9 @@ class UpdateProgressScreen(ModalScreen[None]):
yield Static("Updating Deep Agents CLI", classes="up-title")
with Horizontal(classes="up-status-row"):
self._spinner_widget = Static(
self._spinner.current_frame(), classes="up-spinner"
self._spinner.current_frame(),
classes="up-spinner",
markup=False,
)
yield self._spinner_widget
self._status_widget = Static(
+2 -2
View File
@@ -205,13 +205,13 @@ class TestTokenDisplay:
display = pilot.app.query_one("#tokens-display")
assert str(display.render()) == "... tokens"
async def test_show_pending_tokens_before_count_shows_placeholder(self) -> None:
async def test_show_pending_tokens_before_count_leaves_display_empty(self) -> None:
async with StatusBarApp().run_test() as pilot:
bar = pilot.app.query_one("#status-bar", StatusBar)
bar.show_pending_tokens()
await pilot.pause()
display = pilot.app.query_one("#tokens-display")
assert str(display.render()) == "... tokens"
assert str(display.render()) == ""
async def test_set_tokens_after_pending_restores_display(self) -> None:
"""Regression: set_tokens must refresh even when value is unchanged.
@@ -1,6 +1,7 @@
"""Tests for ThreadSelectorScreen."""
import asyncio
from collections.abc import Coroutine
from typing import Any, ClassVar
from unittest.mock import AsyncMock, MagicMock, patch
@@ -2722,10 +2723,14 @@ class TestBuildThreadMessage:
async def test_fallback_on_timeout(self) -> None:
"""Returns plain string when URL resolution times out."""
app = DeepAgentsApp()
with patch(
"deepagents_cli.app.asyncio.wait_for",
side_effect=TimeoutError,
):
async def _raise_timeout( # noqa: RUF029 # async signature required to match asyncio.wait_for
coro: Coroutine[Any, Any, Any], *_: Any, **__: Any
) -> None:
coro.close()
raise TimeoutError
with patch("deepagents_cli.app.asyncio.wait_for", new=_raise_timeout):
result = await app._build_thread_message("Resumed thread", "t-1")
assert isinstance(result, str)