From 4991d5b3ee997ca1a2f2f81b75d0cd3a7c151db2 Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 14:27:37 -0400 Subject: [PATCH] feat(dashboard): new-agent flow spawns into right tmux pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRD 0021 chunk 3. The `n` flow (PRD 0020 chunk 2) now routes the first claude session of a freshly-started bottle into the right tmux pane when `\$TMUX` is set — same `_attach_in_tmux` state machine the Enter re-attach uses, just with `resume=False` so claude starts fresh. Outside tmux the existing foreground handoff is unchanged. The compose-up phase (`backend.launch.__enter__`) still drops curses for its stderr output; we restore curses BEFORE spawning into the right pane so the dashboard re-renders alongside the new claude session instead of waiting for attach to return. --- claude_bottle/cli/dashboard.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/claude_bottle/cli/dashboard.py b/claude_bottle/cli/dashboard.py index 47b5a25..b35db65 100644 --- a/claude_bottle/cli/dashboard.py +++ b/claude_bottle/cli/dashboard.py @@ -871,12 +871,14 @@ def _new_agent_flow( manifest: Manifest, bottles: dict, agents_now: list[ActiveAgent], + tmux_state: dict | None = None, ) -> str: """Open the picker, prepare + preflight (modal), launch - (enter the context manager but DON'T close it), handoff to - claude. Returns a status-line message for the dashboard footer. - The (cm, bottle) tuple lands in `bottles` keyed by slug; chunks - 3/4 use it for re-attach and explicit stop.""" + (enter the context manager but DON'T close it), then route + the first claude session into the right pane (in-tmux) or + foreground handoff (otherwise). Returns a status-line message + for the dashboard footer. The (cm, bottle) tuple lands in + `bottles` keyed by slug; chunk 4 uses it for explicit stop.""" names = sorted(manifest.agents.keys()) picked = _picker_modal(stdscr, names, _running_counts(bottles, agents_now)) if picked is None: @@ -915,8 +917,10 @@ def _new_agent_flow( backend = get_bottle_backend() # Launch step writes to stderr (image build, network create, # compose up). Get out of curses' way for the duration so - # the lines render cleanly. The handoff stays endwin'd until - # claude exits, then we refresh. + # the lines render cleanly; restore curses immediately + # after — the attach itself may stay out of curses (in-tmux + # spawns into the right pane and returns) or take over + # the terminal (foreground handoff). curses.endwin() try: cm = backend.launch(plan) @@ -927,6 +931,18 @@ def _new_agent_flow( raise bottles[plan.slug] = (cm, bottle, identity) + if _in_tmux() and tmux_state is not None: + # Refresh curses BEFORE spawning into the right pane so + # the dashboard re-renders alongside the new claude + # session. + stdscr.refresh() + return _attach_in_tmux( + stdscr, bottle, plan.slug, + resume=False, tmux_state=tmux_state, + ) + + # Foreground handoff: claude owns the terminal until exit, + # then we restore curses. try: exit_code = attach_claude(bottle, remote_control=False) capture_session_state(identity, exit_code) @@ -1137,7 +1153,9 @@ def _main_loop(stdscr: "curses._CursesWindow") -> None: except Exception as e: status_line = f"manifest load failed: {e}" continue - status_line = _new_agent_flow(stdscr, manifest, bottles, agents) + status_line = _new_agent_flow( + stdscr, manifest, bottles, agents, tmux_state=tmux_state, + ) continue if key in (ord("e"), ord("p")): # PRD 0019 chunk 4: agent-scoped edits. Only fire when