fix(supervise): provision MCP via claude mcp add #25

Merged
didericis merged 9 commits from supervise-mcp-add-via-cli into main 2026-05-25 08:31:17 -04:00
2 changed files with 93 additions and 1 deletions
Showing only changes of commit a9bb34cb77 - Show all commits
+54 -1
View File
@@ -17,6 +17,7 @@ import os
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -344,6 +345,41 @@ def _list_once() -> int:
_REFRESH_INTERVAL_MS = 1000 _REFRESH_INTERVAL_MS = 1000
# How long a newly-arrived proposal stays highlighted (green) in the
# list. Long enough for the operator to notice in their peripheral
# vision, short enough to fade before the queue feels permanently
# noisy.
_NEW_PROPOSAL_HIGHLIGHT_SEC = 5.0
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. Both `first_seen` and `now` may be None (rendered as
not-recent) so the helper is safe in cold-start paths."""
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 _try_init_green() -> int:
"""Initialise a green color pair and return its attr, or 0 if the
terminal doesn't support color. Caller ORs the returned value
into addnstr's attr argument; OR 0 is a no-op."""
try:
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_GREEN, -1)
return curses.color_pair(1)
except curses.error:
return 0
def _main_loop(stdscr: "curses._CursesWindow") -> None: def _main_loop(stdscr: "curses._CursesWindow") -> None:
curses.curs_set(0) curses.curs_set(0)
@@ -353,6 +389,11 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None:
# when the operator hits a key — a tool call landing while the # when the operator hits a key — a tool call landing while the
# operator is just watching wouldn't appear. # operator is just watching wouldn't appear.
stdscr.timeout(_REFRESH_INTERVAL_MS) stdscr.timeout(_REFRESH_INTERVAL_MS)
green_attr = _try_init_green()
# Per-proposal first-seen timestamps drive the "new" highlight.
# We add entries as proposals show up and prune ones that are
# gone (approved / rejected / archived) so the dict stays small.
first_seen: dict[str, float] = {}
selected = 0 selected = 0
status_line = "" status_line = ""
while True: while True:
@@ -360,7 +401,14 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None:
if selected >= len(pending): if selected >= len(pending):
selected = max(0, len(pending) - 1) selected = max(0, len(pending) - 1)
_render(stdscr, pending, selected, status_line) now = time.monotonic()
live_ids = {qp.proposal.id for qp in pending}
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]
_render(stdscr, pending, selected, status_line, first_seen, now, green_attr)
try: try:
key = stdscr.getch() key = stdscr.getch()
@@ -425,6 +473,9 @@ def _render(
pending: list[QueuedProposal], pending: list[QueuedProposal],
selected: int, selected: int,
status_line: str, status_line: str,
first_seen: dict[str, float] | None = None,
now: float | None = None,
green_attr: int = 0,
) -> None: ) -> None:
stdscr.erase() stdscr.erase()
h, w = stdscr.getmaxyx() h, w = stdscr.getmaxyx()
@@ -452,6 +503,8 @@ def _render(
f"{p.justification[:60]}" f"{p.justification[:60]}"
) )
attr = curses.A_REVERSE if i == selected else curses.A_NORMAL 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) stdscr.addnstr(row, 0, line, w - 1, attr)
footer = ( footer = (
+39
View File
@@ -0,0 +1,39 @@
"""Unit: dashboard'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 claude_bottle.cli import dashboard
class TestIsRecent(unittest.TestCase):
def test_just_seen_is_recent(self):
self.assertTrue(dashboard._is_recent("p1", {"p1": 100.0}, now=100.5))
def test_seen_within_window(self):
# Default window is 5s.
self.assertTrue(
dashboard._is_recent("p1", {"p1": 100.0}, now=104.9),
)
def test_seen_past_window_is_not_recent(self):
self.assertFalse(
dashboard._is_recent("p1", {"p1": 100.0}, now=106.0),
)
def test_unknown_proposal_is_not_recent(self):
self.assertFalse(
dashboard._is_recent("p2", {"p1": 100.0}, now=100.5),
)
def test_none_args_safe_default(self):
self.assertFalse(dashboard._is_recent("p1", None, None))
self.assertFalse(dashboard._is_recent("p1", {"p1": 100.0}, None))
self.assertFalse(dashboard._is_recent("p1", None, 100.5))
if __name__ == "__main__":
unittest.main()