feat(dashboard): stop hook clears tmux state + right-pane row marker
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m6s

PRD 0021 chunk 4 (final). Two adjustments to close the
split-pane loop:

1. `_stop_bottle_flow` clears `tmux_state['slug']` when the
   stopped bottle was the right-pane occupant. The pane itself
   stays in place (claude exits with "container not found");
   the operator presses Enter on a different agent to
   repurpose it via respawn-pane.
2. `_render` accepts `right_pane_slug` and marks the matching
   agents-pane row with a `*` prefix + A_BOLD (when it's not
   also the focused row — focused selection still wins for
   visibility). Gives the operator a clear visual link
   between which agent the dashboard says is "active right
   now" and which one is visible to their right.

Wired through `_main_loop`: passes `tmux_state` to
`_stop_bottle_flow` on `x`, and `tmux_state.get('slug')` to
`_render` on every tick.

479 unit tests pass (1 new for the tmux_state-preservation
on non-owned stop). PRD 0021 implementation complete pending
merge.
This commit is contained in:
2026-05-26 14:29:59 -04:00
parent 4991d5b3ee
commit 2ba84c5ba0
2 changed files with 35 additions and 1 deletions
+21 -1
View File
@@ -654,13 +654,21 @@ def _stop_bottle_flow(
stdscr: "curses._CursesWindow",
bottles: dict,
slug: str,
*,
tmux_state: dict | None = None,
) -> str:
"""Explicit per-bottle teardown (PRD 0020 chunk 4). Pops the
(cm, bottle, identity) tuple from the dashboard's bottles
map, snapshots the transcript best-effort, drives the launch
context's __exit__ (compose down + network remove), and
settles the state dir. A non-owned slug is a no-op with a
hint pointing at `./cli.py cleanup`."""
hint pointing at `./cli.py cleanup`.
PRD 0021: clears `tmux_state['slug']` when the stopped
bottle was the right-pane occupant. The pane itself is
left in place — the operator presses Enter on a different
agent to repurpose it (respawn-pane replaces the broken
state)."""
if slug not in bottles:
return (
f"[{slug}] not dashboard-owned — use ./cli.py cleanup"
@@ -687,6 +695,8 @@ def _stop_bottle_flow(
finally:
stdscr.refresh()
settle_state(identity)
if tmux_state is not None and tmux_state.get("slug") == slug:
tmux_state["slug"] = None
return f"[{slug}] stopped"
@@ -1120,6 +1130,7 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None:
agents=agents,
selected_agent=selected_agent,
focus=focus,
right_pane_slug=tmux_state.get("slug"),
first_seen=first_seen, now=now, green_attr=green_attr,
)
@@ -1197,6 +1208,7 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None:
else:
status_line = _stop_bottle_flow(
stdscr, bottles, target.slug,
tmux_state=tmux_state,
)
continue
@@ -1244,6 +1256,7 @@ def _render(
agents: list[ActiveAgent] | None = None,
selected_agent: int = 0,
focus: str = PANE_PROPOSALS,
right_pane_slug: str | None = None,
first_seen: dict[str, float] | None = None,
now: float | None = None,
green_attr: int = 0,
@@ -1325,11 +1338,18 @@ def _render(
if row >= h - 3:
break
line = _format_agent_row(a, w - 1)
in_right_pane = (a.slug == right_pane_slug)
if agents_focused and i == selected_agent:
# Replace the leading " " cursor with "> " and
# highlight the whole row.
line = "> " + line[2:]
attr = curses.A_REVERSE
elif in_right_pane:
# PRD 0021: `*` marks the agent currently in the
# right tmux pane so the operator can see at a
# glance which session is visible to their right.
line = "* " + line[2:]
attr = curses.A_BOLD
else:
attr = curses.A_NORMAL
stdscr.addnstr(row, 0, line, w - 1, attr)
@@ -455,6 +455,20 @@ class TestStopBottleFlow(unittest.TestCase):
self.assertIn("not dashboard-owned", msg)
self.assertIn("./cli.py cleanup", msg)
def test_non_owned_does_not_touch_tmux_state(self):
# PRD 0021: a stop on an unknown slug should never clear
# the right-pane occupant tracking, even if the slugs
# happen to match (defensive — non-owned can't be in the
# right pane via the dashboard's normal flow anyway).
tmux_state = {"pane_id": "%5", "slug": "live-bbb"}
dashboard._stop_bottle_flow(
stdscr=None, # type: ignore[arg-type]
bottles={},
slug="ghost-zzz",
tmux_state=tmux_state,
)
self.assertEqual({"pane_id": "%5", "slug": "live-bbb"}, tmux_state)
class TestOperatorEditFlowGuards(_FakeHomeMixin, unittest.TestCase):
"""Chunk-4 contract: the edit flow refuses when the selected