diff --git a/bot_bottle/cli/supervise.py b/bot_bottle/cli/supervise.py index ca15403..fb786d7 100644 --- a/bot_bottle/cli/supervise.py +++ b/bot_bottle/cli/supervise.py @@ -17,7 +17,6 @@ import os import subprocess import sys import tempfile -import time import traceback from dataclasses import dataclass from datetime import datetime, timezone @@ -59,7 +58,6 @@ from ._common import PROG _REFRESH_INTERVAL_MS = 1000 -_NEW_PROPOSAL_HIGHLIGHT_SEC = 5.0 @dataclass(frozen=True) @@ -99,20 +97,6 @@ def _approval_status(qp: QueuedProposal, verb: str) -> str: return base -def _is_recent( - proposal_id: str, - first_seen: dict[str, float] | None, - now: float | None, -) -> bool: - """True if `proposal_id` was first seen within the highlight window.""" - if first_seen is None or now is None: - return False - started = first_seen.get(proposal_id) - if started is None: - return False - return (now - started) < _NEW_PROPOSAL_HIGHLIGHT_SEC - - def _detail_lines( qp: QueuedProposal, *, @@ -388,21 +372,19 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: curses.curs_set(0) stdscr.timeout(_REFRESH_INTERVAL_MS) green_attr = _try_init_green() - first_seen: dict[str, float] = {} selected = 0 status_line = "" - saw_first_tick = False supervise_pane_id = os.environ.get("TMUX_PANE", "") + seen_ids: set[str] = set() while True: pending = discover_pending() if selected >= len(pending): selected = max(0, len(pending) - 1) - now = time.monotonic() live_ids = {qp.proposal.id for qp in pending} - newly_arrived = live_ids - first_seen.keys() - if saw_first_tick and newly_arrived: + newly_arrived = live_ids - seen_ids + if seen_ids and newly_arrived: try: curses.beep() except curses.error: @@ -413,15 +395,11 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: 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): - del first_seen[stale_id] - saw_first_tick = True + seen_ids = live_ids _render( stdscr, pending, selected, status_line, - first_seen=first_seen, now=now, green_attr=green_attr, + green_attr=green_attr, ) try: @@ -478,8 +456,6 @@ def _render( selected: int, status_line: str, *, - first_seen: dict[str, float] | None = None, - now: float | None = None, green_attr: int = 0, ) -> None: stdscr.erase() @@ -512,8 +488,6 @@ def _render( f"{_proposed_payload_label(p.tool)}" ) attr = curses.A_REVERSE if i == selected else curses.A_NORMAL - if _is_recent(p.id, first_seen, now): - attr |= green_attr stdscr.addnstr(row, 0, line, w - 1, attr) row += 1 if row >= h - 3: diff --git a/docs/prds/0049-strip-dashboard-to-supervisor-tui.md b/docs/prds/0049-strip-dashboard-to-supervisor-tui.md index 91ef8bb..5c316eb 100644 --- a/docs/prds/0049-strip-dashboard-to-supervisor-tui.md +++ b/docs/prds/0049-strip-dashboard-to-supervisor-tui.md @@ -1,5 +1,5 @@ -- **Status:** Active +- **Status:** Draft - **Author:** didericis - **Created:** 2026-06-03 - **Issue:** #174 @@ -127,8 +127,8 @@ problem is everything that got bolted onto that core after. dashboard.py` to `bot_bottle/cli/supervise.py`. The dispatcher in `bot_bottle/cli/__init__.py` and the help text both update. - **Strip the curses loop to proposal-only.** The remaining - surface is: list pending proposals (with the new-arrival bell + - green highlight from PRD 0013), Enter for detail view, + surface is: list pending proposals (with the new-arrival bell + from PRD 0013), Enter for detail view, `a`/`m`/`r` for approve / modify / reject, `q` to quit. No agents pane, no Tab, no agent picker, no `n`/`x`/`e`/`p`, no tmux dispatch, no `bottles` dict on the main loop. @@ -141,10 +141,9 @@ problem is everything that got bolted onto that core after. `operator_edit_allowlist`, and their imports come out. - **Collapse the model module.** `dashboard_model.py`'s proposal-side helpers (`QueuedProposal`, `discover_pending`, - `_approval_status`, `_is_recent`, `_detail_lines`, + `_approval_status`, `_detail_lines`, `_failed_url_host`, `_proposed_payload_label`, - `_suffix_for_tool`, `_REFRESH_INTERVAL_MS`, - `_NEW_PROPOSAL_HIGHLIGHT_SEC`) move back into + `_suffix_for_tool`, `_REFRESH_INTERVAL_MS`) move back into `supervise.py` (CLI) or into `bot_bottle/supervise.py` (the daemon-side module) — wherever they fit. The agents / picker / tmux helpers in that module (`PANE_*`, @@ -205,7 +204,7 @@ ripgrep view, same as today. - `q` / Esc quits. There are no dashboard-owned bottles, so no per-process teardown decision — `q` just exits. -- The new-arrival bell + green highlight + (if in tmux) the +- The new-arrival bell + (if in tmux) the `tmux select-pane` jump back to the supervise pane stay, because they're real wins for the operator's "I was typing at claude and a proposal landed" case. They don't require any of @@ -229,7 +228,7 @@ bot_bottle/cli/supervise.py - reject(qp, *, reason) - QueuedProposal, discover_pending - _detail_lines, _approval_status, - _is_recent, _failed_url_host, + _failed_url_host, _proposed_payload_label, _suffix_for_tool ``` @@ -262,7 +261,7 @@ a clear pointer forward. No PRD body is rewritten. Keep: - `tests/cli/test_dashboard*.py` cases that exercise `discover_pending`, `approve`, `reject`, `_detail_lines`, - `_is_recent`, `_approval_status`, `_failed_url_host`, + `_approval_status`, `_failed_url_host`, `_proposed_payload_label`, `_suffix_for_tool`, `_modify` / `edit_in_editor`. - `tests/cli/test_dashboard_once.py` (or equivalent) — the diff --git a/tests/unit/test_supervise_cli_highlight.py b/tests/unit/test_supervise_cli_highlight.py deleted file mode 100644 index 84dc513..0000000 --- a/tests/unit/test_supervise_cli_highlight.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Unit: supervise's new-proposal highlight window. - -The curses rendering itself is exercised manually; this isolates -the pure decision `is the proposal still in its post-arrival -highlight window?`""" - -import unittest - -from bot_bottle.cli import supervise as supervise_cli - - -class TestIsRecent(unittest.TestCase): - def test_just_seen_is_recent(self): - self.assertTrue(supervise_cli._is_recent("p1", {"p1": 100.0}, now=100.5)) - - def test_seen_within_window(self): - # Default window is 5s. - self.assertTrue( - supervise_cli._is_recent("p1", {"p1": 100.0}, now=104.9), - ) - - def test_seen_past_window_is_not_recent(self): - self.assertFalse( - supervise_cli._is_recent("p1", {"p1": 100.0}, now=106.0), - ) - - def test_unknown_proposal_is_not_recent(self): - self.assertFalse( - supervise_cli._is_recent("p2", {"p1": 100.0}, now=100.5), - ) - - def test_none_args_safe_default(self): - self.assertFalse(supervise_cli._is_recent("p1", None, None)) - self.assertFalse(supervise_cli._is_recent("p1", {"p1": 100.0}, None)) - self.assertFalse(supervise_cli._is_recent("p1", None, 100.5)) - - -if __name__ == "__main__": - unittest.main()