From 933d8cf6c3d2c67457d2bcc6aa94c5c9123fb037 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 15:08:49 -0400 Subject: [PATCH] feat(dashboard): route stop output into right tmux pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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. --- claude_bottle/cli/dashboard.py | 41 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index 8aae861..ccb0205 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -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)