Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions gcode/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = [
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
30 changes: 28 additions & 2 deletions tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 ----------------------------------------------------------


Expand Down