diff --git a/gcode/ui.py b/gcode/ui.py index ab3e783..01d9c47 100644 --- a/gcode/ui.py +++ b/gcode/ui.py @@ -5,6 +5,8 @@ display, a permission gate, and status/error output. """ +import time + import questionary from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -16,7 +18,8 @@ from rich.spinner import Spinner from rich.text import Text -_TRUNCATE_STEP = 80 # re-render Markdown only after this many new characters +_TRUNCATE_STEP = 160 # re-render Markdown only after this many new characters +_TRUNCATE_MIN_INTERVAL = 0.10 # seconds between live updates (max ~10 fps) # Slash commands available in the interactive menu _SLASH_COMMANDS = [ @@ -148,6 +151,7 @@ def prompt(self) -> str: def assistant_start(self) -> None: self._buffer = "" self._last_len = 0 + self._last_update = 0.0 self._live = Live( Spinner("dots", text="Thinking…"), console=self.console, @@ -160,9 +164,14 @@ def token(self, text: str) -> None: if self._live is None: return self._buffer += text - if len(self._buffer) - self._last_len >= _TRUNCATE_STEP: + now = time.monotonic() + if ( + len(self._buffer) - self._last_len >= _TRUNCATE_STEP + and now - self._last_update >= _TRUNCATE_MIN_INTERVAL + ): self._live.update(Markdown(self._buffer)) self._last_len = len(self._buffer) + self._last_update = now def assistant_end(self) -> None: if self._live is None: diff --git a/tests/test_ui.py b/tests/test_ui.py index 46e3bfc..36ff5c7 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -133,16 +133,17 @@ def test_token_renders_markdown_after_threshold(monkeypatch): fake = _FakeLive() monkeypatch.setattr(ui_module, "Live", lambda *args, **kwargs: fake) + monkeypatch.setattr(ui_module, "_TRUNCATE_MIN_INTERVAL", 0.0) ui = RichUI() ui.assistant_start() - # Below the 80-char re-render threshold: no update yet. + # Below the 160-char re-render threshold: no update yet. ui.token("x" * 40) assert fake.updates == [] # Crossing the threshold triggers one Markdown update. - ui.token("y" * 50) # total 90 >= 80 + ui.token("y" * 150) # total 190 >= 160 assert len(fake.updates) == 1 # Ending the stream renders the final text and stops the live region. @@ -151,6 +152,31 @@ def test_token_renders_markdown_after_threshold(monkeypatch): assert len(fake.updates) == 2 +def test_token_throttled_by_time_interval(monkeypatch): + from gcode import ui as ui_module + + fake = _FakeLive() + monkeypatch.setattr(ui_module, "Live", lambda *args, **kwargs: fake) + monkeypatch.setattr(ui_module, "_TRUNCATE_STEP", 1) + monkeypatch.setattr(ui_module, "_TRUNCATE_MIN_INTERVAL", 60.0) + + clock = iter([10.0, 10.05, 70.0]) + monkeypatch.setattr(ui_module.time, "monotonic", lambda: next(clock)) + + ui = RichUI() + ui.assistant_start() + + ui.token("a") # 1 char but only 10s elapsed since 0.0 -> throttled + ui.token("b") # still < 60s elapsed -> throttled + assert len(fake.updates) == 0 + + ui.token("c") # 70s elapsed -> update fires + assert len(fake.updates) == 1 + + ui.assistant_end() + assert fake.stopped + + # -- tool display ----------------------------------------------------------