From 9ac05c1a63b7b53fb76adb6a407b4720dbf58bed Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 15:55:47 -0400 Subject: [PATCH 1/2] 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( From 3a7b7d054b8b642865e59349fbcadcf21b4e9942 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 16:04:23 -0400 Subject: [PATCH 2/2] feat(dashboard): auto-focus dashboard pane + proposals on new arrival MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a fresh proposal arrives, the dashboard now also: - Runs `tmux select-pane -t \$TMUX_PANE` (the dashboard's own pane id, captured at startup) so tmux focus jumps to the dashboard from wherever the operator was (typically claude in the right pane). - Flips internal focus to PANE_PROPOSALS so j/k navigates the queued items immediately. - Lands the selected cursor on the first new proposal — proposals are sorted by arrival ascending, so the earliest new arrival in the batch gets the cursor. Stacks with the bell + label highlight from the previous commit. The operator gets: 1. Audible bell (or tmux activity marker) 2. Tmux focus on the dashboard pane 3. Dashboard's internal focus on the proposals list 4. Cursor on the actual new proposal 5. Pane label flashing `(new!)` in bold green — all without leaving the keyboard. --- claude_bottle/cli/dashboard.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index 296b51a..00011b4 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -1356,6 +1356,11 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: # pre-existing queue entries on its first poll; those # shouldn't ring the bell as if they just arrived. saw_first_tick = False + # The dashboard's own tmux pane id (tmux sets `$TMUX_PANE` + # per-pane). Captured at startup so a new-proposal arrival + # can `tmux select-pane` back to the dashboard from + # whatever pane the operator is currently in. + dashboard_pane_id = os.environ.get("TMUX_PANE", "") while True: pending = discover_pending() if selected >= len(pending): @@ -1367,16 +1372,29 @@ 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). + # Detect proposals we've never seen before. Triggers: + # - terminal bell (`curses.beep` → tmux's monitor-bell) + # - tmux focus jump to the dashboard pane (so the + # operator notices even if they were typing at claude) + # - dashboard's internal focus flip to the proposals + # pane (so j/k navigates the queued items immediately) newly_arrived = live_ids - first_seen.keys() if saw_first_tick and newly_arrived: try: curses.beep() except curses.error: pass + if dashboard_pane_id and _in_tmux(): + _tmux_select_pane(dashboard_pane_id) + focus = PANE_PROPOSALS + # Land the cursor on the first new proposal so the + # operator can act immediately. Proposals are sorted + # by arrival_timestamp ascending; find the lowest + # index whose id is in `newly_arrived`. + for i, qp in enumerate(pending): + if qp.proposal.id in newly_arrived: + selected = i + break for proposal_id in live_ids: first_seen.setdefault(proposal_id, now) for stale_id in list(first_seen.keys() - live_ids):