From 4e4051f420b26af2a927d716d4b355db6d5fbc1e Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 07:48:24 -0400 Subject: [PATCH] fix(dashboard): auto-refresh the TUI every 1s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main loop blocked on stdscr.getch() until the operator hit a key — a tool call landing in the queue while the operator was just watching wouldn't appear on the screen. The operator had to press any key to trigger a re-render and see the new proposal. Switch to stdscr.timeout(1000): getch returns -1 after 1s if no key was pressed, and the loop re-renders with the latest discover_pending() result. CPU cost is trivial; the loop body is ~one filesystem scan + curses draw per second. Also restructure status_line lifecycle: was cleared right after every render, which meant a timeout-driven re-render would wipe the message ~1s after the operator's keystroke set it. Now status_line is cleared only on actual key press, so messages like "approved cred-proxy-block for [dev-xyz]" persist until the operator does something else. Detail view + prompt view are unchanged — they're modal, the underlying proposal data doesn't move, and getstr can't tolerate a re-render mid-input. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/cli/dashboard.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index 4ced7e6..d686266 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -342,9 +342,17 @@ def _list_once() -> int: return 0 +_REFRESH_INTERVAL_MS = 1000 + + def _main_loop(stdscr: "curses._CursesWindow") -> None: curses.curs_set(0) - stdscr.nodelay(False) + # Auto-refresh: getch() returns -1 after the timeout if no key + # was pressed, so the loop re-renders with any newly-arrived + # proposals every ~1s. Without this the screen only updates + # when the operator hits a key — a tool call landing while the + # operator is just watching wouldn't appear. + stdscr.timeout(_REFRESH_INTERVAL_MS) selected = 0 status_line = "" while True: @@ -353,13 +361,22 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: selected = max(0, len(pending) - 1) _render(stdscr, pending, selected, status_line) - status_line = "" try: key = stdscr.getch() except KeyboardInterrupt: return + if key == -1: + # Timeout fired — re-render with fresh queue. Status_line + # is left intact so messages from a prior keystroke stay + # readable until the operator actually does something else. + continue + + # Real keystroke: clear any stale status before dispatching + # so the next render reflects what just happened. + status_line = "" + if key in (ord("q"), 27): # q or ESC return if key == ord("e"):