feat(dashboard): route stop output into right tmux pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s

PRD 0021 follow-up. Mirrors the bringup-into-right-pane fix
on the explicit-stop path: when `\$TMUX` is set, the stop
flow respawns the right pane with `tail -F
state/<slug>/teardown.log` (via `_ensure_right_pane` —
reuses the existing right pane if it's the agent's claude
session) and redirects fd 2 to that log for the duration of
`capture_session_state` + `cm.__exit__`. compose-down +
network-remove messages stream into the right pane.

After `settle_state` removes the state dir, the tail keeps
its buffered output visible (tail -F handles file removal
gracefully); the next attach respawns the pane with claude.

Falls back to the existing curses-endwin path on tmux
failure, or when the dashboard isn't in tmux at all.
This commit is contained in:
2026-05-26 15:08:49 -04:00
parent e90d7dba76
commit 933d8cf6c3
+37 -4
View File
@@ -693,10 +693,8 @@ def _stop_bottle_flow(
f"[{slug}] not dashboard-owned — use ./cli.py cleanup"
)
cm, _bottle, identity = bottles.pop(slug)
# compose-down writes to stderr; drop curses so the lines
# render cleanly. Same pattern as the attach handoff.
curses.endwin()
try:
def _do_teardown() -> None:
# Best-effort snapshot before teardown so the operator
# can still inspect the agent's last state via the
# preserved transcript dir even after explicit stop.
@@ -711,6 +709,41 @@ def _stop_bottle_flow(
cm.__exit__(None, None, None)
except BaseException:
pass
if _in_tmux() and tmux_state is not None:
# Mirror the bringup path: route compose-down + state-
# settle output into the right pane via `tail -F` + fd-2
# redirect. Reuses any existing right pane (which is
# probably the agent's own claude session) via
# _ensure_right_pane → respawn-pane. Tail-F handles the
# state-dir-being-removed-mid-tail case gracefully.
log_path = bottle_state_dir(slug) / "teardown.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text("")
pane_id = _ensure_right_pane(
tmux_state, ["tail", "-F", str(log_path)],
)
if pane_id is not None:
tmux_state["slug"] = slug
try:
with _redirect_stderr_to_file(log_path):
_do_teardown()
except BaseException:
pass
settle_state(identity)
# Right pane keeps tailing the (now-removed) log — its
# final buffered output stays visible until the next
# attach respawns it.
tmux_state["slug"] = None
return f"[{slug}] stopped"
# tmux failed; fall through to the curses-endwin path.
# Non-tmux: compose-down output writes to the dashboard's
# terminal directly. Drop curses so the lines render cleanly,
# restore after.
curses.endwin()
try:
_do_teardown()
finally:
stdscr.refresh()
settle_state(identity)