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

16 Commits

Author SHA1 Message Date
didericis ac914b6cb9 feat(dashboard): focus right pane after new-agent bringup completes
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m10s
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.
2026-05-26 15:37:07 -04:00
didericis 1a1ba6abd5 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
`--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).
2026-05-26 15:34:21 -04:00
didericis 7e20d75f00 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
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.
2026-05-26 15:25:22 -04:00
didericis 8d6e382af5 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
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).
2026-05-26 15:21:20 -04:00
didericis 9622bdc619 feat(dashboard): default focus to agents pane
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m9s
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.
2026-05-26 15:16:06 -04:00
didericis 9646bc1c4c refactor(dashboard): extract _route_op_to_right_pane helper
test / unit (pull_request) Successful in 19s
test / integration (pull_request) Successful in 1m7s
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.
2026-05-26 15:13:20 -04:00
didericis 933d8cf6c3 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.
2026-05-26 15:08:49 -04:00
didericis e90d7dba76 fix(dashboard): repaint stdscr immediately after modal closes
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
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.
2026-05-26 15:01:56 -04:00
didericis 0936c40428 fix(dashboard): reuse existing right pane on new-agent start
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m13s
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.
2026-05-26 14:50:56 -04:00
didericis 83ec9669c9 feat(dashboard): route launch output into right tmux pane
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m8s
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.
2026-05-26 14:41:53 -04:00
didericis 2ba84c5ba0 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
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.
2026-05-26 14:29:59 -04:00
didericis 4991d5b3ee feat(dashboard): new-agent flow spawns into right tmux pane
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.
2026-05-26 14:27:37 -04:00
didericis 9944878277 feat(dashboard): tmux split-pane helpers + Enter dispatch
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`).
2026-05-26 14:26:40 -04:00
didericis 2303cbc0be refactor(bottle): extract claude_docker_argv from exec_claude
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m10s
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).
2026-05-26 14:21:04 -04:00
didericis e5316be454 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
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.
2026-05-26 14:18:24 -04:00
didericis 8b8d668602 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
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).
2026-05-26 14:14:02 -04:00