From e4adb67ed89e671bf90d9737fc464ded5f13a5f1 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Tue, 10 Mar 2026 06:02:07 -0700 Subject: [PATCH] fix(display): rate-limit spinner flushes to prevent line spam under patch_stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The KawaiiSpinner animation would occasionally spam dozens of duplicate lines instead of overwriting in-place with \r. This happened because prompt_toolkit's StdoutProxy processes each flush() as a separate run_in_terminal() call — when the write thread is slow (busy event loop during long tool executions), each \r frame gets its own call, and the terminal layout save/restore between calls breaks the \r overwrite semantics. Fix: rate-limit flush() calls to at most every 0.4s. Between flushes, \r-frame writes accumulate in StdoutProxy's buffer. When flushed, they concatenate into one string (e.g. \r frame1 \r frame2 \r frame3) and are written in a single run_in_terminal() call where \r works correctly. The spinner still animates (flush ~2.5x/sec) but each flush batches ~3 frames, guaranteeing the \r collapse always works. Most visible with execute_code and terminal tools (3+ second executions). --- agent/display.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/agent/display.py b/agent/display.py index 581cde562..120cfd12a 100644 --- a/agent/display.py +++ b/agent/display.py @@ -206,6 +206,7 @@ class KawaiiSpinner: self.frame_idx = 0 self.start_time = None self.last_line_len = 0 + self._last_flush_time = 0.0 # Rate-limit flushes for patch_stdout compat # Capture stdout NOW, before any redirect_stdout(devnull) from # child agents can replace sys.stdout with a black hole. self._out = sys.stdout @@ -236,7 +237,18 @@ class KawaiiSpinner: else: line = f" {frame} {self.message} ({elapsed:.1f}s)" pad = max(self.last_line_len - len(line), 0) - self._write(f"\r{line}{' ' * pad}", end='', flush=True) + # Rate-limit flush() calls to avoid spinner spam under + # prompt_toolkit's patch_stdout. Each flush() pushes a queue + # item that may trigger a separate run_in_terminal() call; if + # items are processed one-at-a-time the \r overwrite is lost + # and every frame appears on its own line. By flushing at + # most every 0.4s we guarantee multiple \r-frames are batched + # into a single write, so the terminal collapses them correctly. + now = time.time() + should_flush = (now - self._last_flush_time) >= 0.4 + self._write(f"\r{line}{' ' * pad}", end='', flush=should_flush) + if should_flush: + self._last_flush_time = now self.last_line_len = len(line) self.frame_idx += 1 time.sleep(0.12)