docs(prd-0021): dashboard as left tmux pane, selected agent as right pane #49
Reference in New Issue
Block a user
Delete Branch "dashboard-tmux-split-pane"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
PRD for tmux integration: when the dashboard runs inside tmux (
\$TMUXset), lay it out as the left pane with the selected agent's claude session in the right pane. Pressing Enter respawns the right pane with the focused agent's session (using--continueso the conversation continues); pressingnstarts a new agent directly into the right pane. Outside tmux, falls back to today's handoff.Mechanism
right_pane_id+right_pane_slugtracked on the main loop. First attach →tmux split-window -h -P -F '#{pane_id}'captures the new pane's id. Subsequent attaches →tmux respawn-pane -k -t <id> docker exec -it … claude --continue._tmux_pane_existscheck viatmux list-paneshandles the operator manually closing the right pane (falls back to a fresh split).Three failure modes handled gracefully (tmux binary missing → handoff; tmux subcommand non-zero → handoff for that keypress; right pane manually closed → fresh split next attach).
Sized into 4 chunks
DockerBottle.claude_docker_argvrefactor — pure split ofexec_claudeso both foreground and tmux paths use the same argv (preserves--append-system-prompt-file)._attach_to_bottledispatch.*prefix on the agents-pane row whose bottle is currently in the right pane).Open questions
7 called out — most load-bearing: split direction (
-hvs-vfor narrow / wide terminals), pane sizing, whether to auto-exec into a fresh tmux session when launched outside one (v1 says no).PRD 0021 chunk 2. New tmux integration: when `\$TMUX` is set and the operator presses Enter on a focused agent row, the dashboard spawns / respawns the right pane with that bottle's claude session instead of taking over the terminal via curses.endwin. Mechanics: - `_in_tmux()` — true when `\$TMUX` is set. - `_tmux_split_pane_create` — first attach: `tmux split-window -h -P -F '#{pane_id}'` opens a right pane and prints its id for tracking. - `_tmux_respawn_pane` — subsequent attaches: `tmux respawn-pane -k -t <id>` swaps the content without re-splitting. - `_tmux_pane_exists` — `tmux list-panes` check before respawn so a manually-closed pane gracefully falls back to a fresh split. - `_attach_in_tmux` — owns the create-or-respawn state machine, mutates `tmux_state` ({pane_id, slug}) so the main loop tracks the right-pane occupant. - `_attach_via_handoff` — the previous curses-endwin path, extracted as the fallback when tmux is missing or fails. - `_attach_to_bottle` dispatches: in tmux + state available → `_attach_in_tmux`; otherwise → handoff. Main loop gets `tmux_state: dict = {"pane_id": None, "slug": None}`. Chunks 3 + 4 wire it through the new-agent flow and the stop hook. `FileNotFoundError`-safe `subprocess.run` calls around every tmux invocation — a missing tmux binary cleanly falls back to the handoff for that keypress. 478 unit tests pass (10 new for the pure argv builders + `_claude_runtime_args`).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.PRD 0021 follow-up. When starting a new agent via `n` while in tmux, the dashboard now: 1. Pre-creates the right pane with `tail -F state/<slug>/bringup.log`. 2. Redirects fd 2 (stderr) to that log file via dup2 — affects both Python `info()` calls AND subprocess inheritors' stderr (docker compose up, network creates, provision). 3. Runs `backend.launch().__enter__()` with the redirect in place; everything streams into the right pane via tail. 4. Restores stderr. 5. Respawns the right pane (tail → claude session). Net effect: dashboard pane stays uncluttered during bringup, and the operator watches the compose-up + provision output in the same pane that's about to hold the claude session — no visual handoff between "starting" and "started." Curses never needs to come down on the tmux path (the pane is already created in the dashboard's neighbor pane, and stderr is redirected away from the terminal entirely). If `_tmux_split_pane_tail` fails (tmux missing, server died), falls through to the existing curses-endwin handoff so the operator still gets a session.Both `_new_agent_flow` (bringup) and `_stop_bottle_flow` (teardown) were doing the same five-step dance: open the log path, mkdir parents, empty the file, ensure the right pane is tailing it, redirect fd 2 to the same file. Extract into a context manager: with _route_op_to_right_pane(tmux_state, slug, log_name) as routed: if routed: <run op> Yields True when routing succeeded (fd 2 redirected, pane tailing), False on fallback conditions (not in tmux, no tmux_state, or tmux failed to spawn a pane). The fallback paths still differ between callers — bringup follows up with `_attach_in_tmux`, teardown does the curses-endwin compose-down — so the helper stops at "is stderr routed or not" and lets callers branch from there. Net diff: ~60 lines deleted, the routing-to-right-pane concept now lives in one place.After `x` stops a dashboard-owned bottle, slide focus to the next agent in the agents pane (the one filling the stopped row, or the new last row if the stopped was last) and respawn the right pane with that agent's claude session via `--continue`. If no agents remain, close the right pane via `tmux kill-pane`. Two new helpers: - `_tmux_close_right_pane(tmux_state)` — kills the tracked pane (if it exists) and clears pane_id / slug. - `_pick_next_after_stop(agents_before, selected_index, stopped_slug)` — pure chooser returning (new_index, agent) or None. Tested directly. Outside tmux, only the selected_agent index slides; no auto-attach (foreground handoff would take over the terminal, disruptive). 485 unit tests pass (6 new for the pick helper).The Enter key on a focused agents-pane row is the operator's explicit "I want to interact with this agent" signal — after respawning the right pane with claude, move tmux's keyboard focus to that pane so the operator can start typing immediately. Without this, every Enter required a manual tmux nav (C-b →) to actually use the session. Mechanics: - `_attach_in_tmux` gains `focus_right_pane: bool = False`. - When True, runs `tmux select-pane -t <pane_id>` after the respawn. - `_attach_to_bottle` (the Enter handler's helper) passes True. - Other callers (new-agent flow, stop's auto-attach) leave it False so the operator stays in the dashboard for follow-up navigation. `_tmux_select_pane` is a small subprocess wrapper, best-effort on failure.