docs(prd-0021): dashboard as left tmux pane, selected agent as right pane #49

Merged
didericis merged 16 commits from dashboard-tmux-split-pane into main 2026-05-26 15:40:55 -04:00
Owner

Summary

PRD for tmux integration: when the dashboard runs inside tmux (\$TMUX set), 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 --continue so the conversation continues); pressing n starts a new agent directly into the right pane. Outside tmux, falls back to today's handoff.

Mechanism

right_pane_id + right_pane_slug tracked 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_exists check via tmux list-panes handles 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

  1. DockerBottle.claude_docker_argv refactor — pure split of exec_claude so both foreground and tmux paths use the same argv (preserves --append-system-prompt-file).
  2. tmux helpers + pane state + _attach_to_bottle dispatch.
  3. New-agent flow integration.
  4. Stop integration + right-pane indicator (* prefix on the agents-pane row whose bottle is currently in the right pane).

Open questions

7 called out — most load-bearing: split direction (-h vs -v for narrow / wide terminals), pane sizing, whether to auto-exec into a fresh tmux session when launched outside one (v1 says no).

## Summary PRD for tmux integration: when the dashboard runs inside tmux (`\$TMUX` set), 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 `--continue` so the conversation continues); pressing `n` starts a new agent directly into the right pane. Outside tmux, falls back to today's handoff. ## Mechanism `right_pane_id` + `right_pane_slug` tracked 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_exists` check via `tmux list-panes` handles 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 1. `DockerBottle.claude_docker_argv` refactor — pure split of `exec_claude` so both foreground and tmux paths use the same argv (preserves `--append-system-prompt-file`). 2. tmux helpers + pane state + `_attach_to_bottle` dispatch. 3. New-agent flow integration. 4. Stop integration + right-pane indicator (`*` prefix on the agents-pane row whose bottle is currently in the right pane). ## Open questions 7 called out — most load-bearing: split direction (`-h` vs `-v` for narrow / wide terminals), pane sizing, whether to auto-exec into a fresh tmux session when launched outside one (v1 says no).
didericis added 1 commit 2026-05-26 14:14:19 -04:00
docs(prd-0021): dashboard as left tmux pane, selected agent as right pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m8s
8b8d668602
Draft a PRD that tightens PR #48's tmux integration from
"one new window per attach" to "one persistent right pane that
the dashboard's selection drives." Inside tmux (`\$TMUX` set):
dashboard in the left pane; pressing Enter or `n` spawns
claude in the right pane via `tmux split-window` on first
attach, then `tmux respawn-pane` on subsequent attaches so the
operator-focused agent is always the visible one.

Outside tmux: falls back to today's handoff. Opt-in by
environment; no flag.

Sized into 4 chunks (pane state + create → respawn → stop
integration → supersede PR #48's new-window). Seven open
questions called out, the biggest being whether the dashboard
should auto-exec into a fresh tmux session when launched
outside one (v1 says no — operators start tmux themselves).
didericis added 1 commit 2026-05-26 14:18:27 -04:00
docs(prd-0021): rewrite as standalone — no references to closed PR #48
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m6s
e5316be454
PR #48 closed; treat the implementation as starting from
main, where no tmux integration exists yet. The PRD now
describes the full design (including the `_in_tmux` detection
+ helper scaffolding) as fresh work. Sized into 4 chunks:
`claude_docker_argv` refactor → tmux helpers + pane state +
`_attach_to_bottle` dispatch → new-agent flow → stop +
indicator.

Same design as before — opt-in by `\$TMUX`, split-window-then-
respawn, falls back to handoff on tmux failure or missing
binary. No external references to PR #48.
didericis added 1 commit 2026-05-26 14:21:08 -04:00
refactor(bottle): extract claude_docker_argv from exec_claude
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m10s
2303cbc0be
PRD 0021 chunk 1. The tmux split-pane helpers (chunk 2+) need
the same docker-exec argv that `exec_claude` builds — including
the `--append-system-prompt-file <path>` flag the bottle's
provisioner copies into place. Extract the argv construction
into a pure `claude_docker_argv(argv, *, tty)` method so both
foreground (`subprocess.run`) and tmux paths
(`tmux respawn-pane …`) build from the same source.

`exec_claude` becomes a one-liner that runs subprocess.run on
the argv. No behavior change; 472 unit tests pass (7 new for
the pure builder).
didericis added 3 commits 2026-05-26 14:30:02 -04:00
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 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.
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
2ba84c5ba0
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.
didericis added 1 commit 2026-05-26 14:41:55 -04:00
feat(dashboard): route launch output into right tmux pane
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m8s
83ec9669c9
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.
didericis added 1 commit 2026-05-26 14:51:00 -04:00
fix(dashboard): reuse existing right pane on new-agent start
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m13s
0936c40428
PRD 0021 follow-up. The new-agent flow was calling a dedicated
`_tmux_split_pane_tail` that ALWAYS created a new pane —
so every `n` start spawned a fresh right pane next to any
existing one, accumulating panes instead of reusing them.

Replace with a generic `_ensure_right_pane(tmux_state, argv)`
that respawns the dashboard's tracked right pane if one is
alive, splits a new one only when none is tracked or the
tracked pane was closed. Both the new-agent tail-during-
bringup path AND the existing claude-attach path now route
through this helper.

Net effect: starting a second agent reuses the same right
pane — bringup tail replaces the prior claude session,
then claude (for the new agent) replaces the tail. Closing
the right pane manually via `C-b x` still triggers a fresh
split on the next attach.
didericis added 1 commit 2026-05-26 15:01:59 -04:00
fix(dashboard): repaint stdscr immediately after modal closes
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
e90d7dba76
After the operator pressed `y` on the preflight modal (or
picked an agent in the picker), the modal's curses sub-window
stayed on screen until the dashboard's main loop ticked again
— which during a 5-10s launch made it look like the
confirmation never registered.

Add `_erase_modal` (touchwin + refresh on stdscr) and call it
at every exit from `_preflight_modal` and `_picker_modal`.
The pre-modal frame buffered on stdscr immediately overwrites
the sub-window's area; the launch proceeds with a clean
dashboard underneath.
didericis added 1 commit 2026-05-26 15:08:51 -04:00
feat(dashboard): route stop output into right tmux pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
933d8cf6c3
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.
didericis added 1 commit 2026-05-26 15:13:23 -04:00
refactor(dashboard): extract _route_op_to_right_pane helper
test / unit (pull_request) Successful in 19s
test / integration (pull_request) Successful in 1m7s
9646bc1c4c
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.
didericis added 1 commit 2026-05-26 15:16:08 -04:00
feat(dashboard): default focus to agents pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m9s
9622bdc619
The dashboard is primarily an agent-management surface
(PRD 0020 + 0021); landing on the proposals pane was a holdover
from when proposals were the only thing the dashboard showed.
Default focus is now `PANE_AGENTS`, so j/k navigates the agents
list immediately on launch — the operator Tabs to proposals
when something queues. Focus choice still persists across
operations.
didericis added 1 commit 2026-05-26 15:21:23 -04:00
feat(dashboard): auto-focus next agent on stop, or close pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m5s
8d6e382af5
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).
didericis added 1 commit 2026-05-26 15:25:26 -04:00
feat(dashboard): focus right pane on Enter re-attach (in tmux)
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m8s
7e20d75f00
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.
didericis added 1 commit 2026-05-26 15:34:24 -04:00
fix(dashboard): fall back to fresh claude when --continue has no session
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
1a1ba6abd5
`--continue` exits non-zero when an agent has been spun up but
never typed at — there's no transcript to resume. Re-attaching
to such an agent via Enter (tmux mode) was crashing the pane.

Wrap the resume invocation in `sh -c '<cmd> --continue || <cmd>'`
so a failed `--continue` cleanly falls through to a fresh
claude. The shell adds microseconds and the fallback only
kicks in when --continue would have failed anyway.

New `_build_resume_argv_with_fallback(bottle)` builds the
shell-wrapped docker exec argv with proper shlex quoting (so
paths-with-spaces in `--append-system-prompt-file` survive).
Only the tmux re-attach path uses it; first-attach + foreground
handoff are unchanged.

489 unit tests pass (4 new for the fallback builder).
didericis added 1 commit 2026-05-26 15:37:11 -04:00
feat(dashboard): focus right pane after new-agent bringup completes
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m10s
ac914b6cb9
The new-agent (`n`) flow's tmux branch was leaving keyboard
focus in the dashboard pane after compose-up + provision
finished and claude landed in the right pane — same situation
as Enter re-attach before its `focus_right_pane` fix. The
operator just spun an agent up; they want to type at it.

Pass `focus_right_pane=True` to `_attach_in_tmux` from the
new-agent flow. `tmux select-pane` runs after the respawn.
didericis merged commit 33f1b40479 into main 2026-05-26 15:40:55 -04:00
Sign in to join this conversation.