From 9ac05c1a63b7b53fb76adb6a407b4720dbf58bed Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 15:55:47 -0400 Subject: [PATCH] feat(dashboard): highlight proposals pane + bell on new proposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a fresh proposal lands in the supervise queue, the dashboard: 1. Rings the terminal bell via `curses.beep()` so tmux's `monitor-bell` (or the terminal's own bell-on-activity) surfaces a notice in the dashboard pane even when the operator is focused on claude in the right pane. 2. Bolds + green-attrs the `proposals:` pane label and suffixes it with `(new!)` so a glance at the dashboard screen catches the alert at a glance. The highlight tracks the existing per-row green-highlight window (`_NEW_PROPOSAL_HIGHLIGHT_SEC`). The bell only fires for NEWLY arrived proposals after the first tick — pre-existing queue entries on dashboard startup don't ring. --- claude_bottle/cli/dashboard.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index b99353c..296b51a 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -1352,6 +1352,10 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: if manifest_cache[0] is None: manifest_cache[0] = Manifest.resolve(USER_CWD) return manifest_cache[0] + # First-tick guard: a brand-new dashboard finds any + # pre-existing queue entries on its first poll; those + # shouldn't ring the bell as if they just arrived. + saw_first_tick = False while True: pending = discover_pending() if selected >= len(pending): @@ -1363,10 +1367,21 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: now = time.monotonic() live_ids = {qp.proposal.id for qp in pending} + # Detect proposals we've never seen before — used to ring + # the terminal bell (so tmux's monitor-bell or the + # terminal's own bell-on-activity surface a notice when + # the operator isn't looking at the dashboard pane). + newly_arrived = live_ids - first_seen.keys() + if saw_first_tick and newly_arrived: + try: + curses.beep() + except curses.error: + pass for proposal_id in live_ids: first_seen.setdefault(proposal_id, now) for stale_id in list(first_seen.keys() - live_ids): del first_seen[stale_id] + saw_first_tick = True _render( stdscr, pending, selected, status_line, @@ -1541,10 +1556,22 @@ def _render( # ----- proposals pane (top) ----- row = 2 + # When any proposal is in the recent-arrival window (the + # individual rows are green-highlighted by the existing logic), + # also highlight the pane label so the alert is visible at a + # glance even when the operator is focused elsewhere. + proposals_have_recent = any( + _is_recent(qp.proposal.id, first_seen, now) for qp in pending + ) proposals_label = "proposals:" + if proposals_have_recent: + proposals_label += " (new!)" if proposals_focused: proposals_label += " (focused)" - stdscr.addnstr(row, 0, proposals_label, w - 1, curses.A_DIM) + label_attr = curses.A_DIM + if proposals_have_recent: + label_attr = curses.A_BOLD | green_attr + stdscr.addnstr(row, 0, proposals_label, w - 1, label_attr) row += 1 if not pending: stdscr.addnstr(