docs(prd-0020): start + attach to agents from the dashboard #44

Merged
didericis merged 4 commits from dashboard-start-attach-agents into main 2026-05-26 03:27:02 -04:00
Owner

Summary

PRD that collapses the two-terminal workflow (./cli.py start in one terminal, ./cli.py dashboard in another) into a single dashboard invocation. From inside the dashboard you can:

  • press n to open an agent-picker modal and spin up a new bottle
  • press Enter on a dashboard-owned agents-pane row to re-attach via docker exec -it claude (the "handoff" mechanism from the recent research doc)
  • press x to explicitly stop just that bottle
  • press q to quit, which tears down every bottle the dashboard started

The critical design shift: the bottle's lifetime is owned by the dashboard process, not by any single claude session. Exit claude → back to dashboard with the bottle still running. Today's ./cli.py start couples them tightly via its ExitStack; the dashboard owns one ExitStack across the whole session.

Mechanism (from PR #43's research)

This PRD picks option 1 (handoff) from docs/research/claude-code-pane-in-dashboard.md:

curses.endwin()
bottle.exec_claude(["--dangerously-skip-permissions"], tty=True)
stdscr.refresh()

Option 2 (embedded emulator) and option 3 (external multiplexer) stay out of scope.

Sized into 5 chunks

  1. Refactor _launch_bottle so prepare + preflight + attach are separable pieces the dashboard can call piecewise (no behavior change).
  2. Agent picker modal + new-agent flow (n key).
  3. Re-attach on Enter for dashboard-owned agents.
  4. Explicit per-bottle stop (x).
  5. Quit-cleanup (q calls stack.close() before exit).

Open questions

Seven, largest being modal-vs-drop-and-resume for the preflight Y/N (prototype both during chunk 2) and what to do when a claude session exits because the container died unexpectedly (snapshot + preserve + status-line message, but the dashboard hook isn't designed yet).

## Summary PRD that collapses the two-terminal workflow (`./cli.py start` in one terminal, `./cli.py dashboard` in another) into a single dashboard invocation. From inside the dashboard you can: - press `n` to open an agent-picker modal and spin up a new bottle - press `Enter` on a dashboard-owned agents-pane row to re-attach via `docker exec -it claude` (the "handoff" mechanism from the recent research doc) - press `x` to explicitly stop just that bottle - press `q` to quit, which tears down every bottle the dashboard started The critical design shift: **the bottle's lifetime is owned by the dashboard process, not by any single claude session.** Exit claude → back to dashboard with the bottle still running. Today's `./cli.py start` couples them tightly via its ExitStack; the dashboard owns one ExitStack across the whole session. ## Mechanism (from PR #43's research) This PRD picks **option 1 (handoff)** from `docs/research/claude-code-pane-in-dashboard.md`: ``` curses.endwin() bottle.exec_claude(["--dangerously-skip-permissions"], tty=True) stdscr.refresh() ``` Option 2 (embedded emulator) and option 3 (external multiplexer) stay out of scope. ## Sized into 5 chunks 1. Refactor `_launch_bottle` so prepare + preflight + attach are separable pieces the dashboard can call piecewise (no behavior change). 2. Agent picker modal + new-agent flow (`n` key). 3. Re-attach on Enter for dashboard-owned agents. 4. Explicit per-bottle stop (`x`). 5. Quit-cleanup (`q` calls `stack.close()` before exit). ## Open questions Seven, largest being modal-vs-drop-and-resume for the preflight Y/N (prototype both during chunk 2) and what to do when a claude session exits because the container died unexpectedly (snapshot + preserve + status-line message, but the dashboard hook isn't designed yet).
didericis added 1 commit 2026-05-26 02:59:59 -04:00
docs(prd-0020): start + attach to agents from the dashboard
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
ec20293c0a
Draft a PRD that turns the dashboard into the operator's single
surface — collapses today's two-terminal workflow (one for
`./cli.py start`, one for `./cli.py dashboard`) into a single
dashboard invocation that can spin up new agents, re-attach to
ones it already spun up, and explicitly stop them.

Picks the "handoff" mechanism from `docs/research/claude-code-
pane-in-dashboard.md` (curses.endwin → docker exec -it claude
→ stdscr.refresh) and crucially decouples the bottle's lifetime
from any single claude session: exit claude → back to dashboard
with the bottle still running; quit dashboard → tear down every
bottle the dashboard owns.

Sized into 5 chunks (refactor → picker + new-agent → re-attach
→ explicit stop → quit-cleanup). Seven open questions called
out, the biggest being modal-vs-drop-and-resume for the
preflight Y/N inside curses.
didericis added 2 commits 2026-05-26 03:12:33 -04:00
refactor(start): extract prepare_with_preflight + attach_claude
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
a56be6beb5
PRD 0020 chunk 1. `cli/start.py`'s `_launch_bottle` did three
things in one function: prepare + preflight, attach claude, and
settle state on teardown. Split them so the dashboard (PRD 0020
chunk 2+) can reuse the prepare + attach pieces piecewise
without going through the CLI's one-shot orchestrator:

  - `prepare_with_preflight(spec, *, stage_dir, render_preflight,
    prompt_yes, dry_run)` — injects render + prompt callables so
    the CLI binds them to stderr/stdin while the dashboard binds
    them to a curses modal. Returns `(plan, identity)`; identity
    is set after `backend.prepare` returns so callers can reap
    the prepare-time state dir on abort via `settle_state` in
    their finally — preserving today's preflight-N cleanup.
  - `attach_claude(bottle, *, remote_control)` — runs claude
    inside the bottle and returns its exit code. The dashboard
    calls this from inside a `curses.endwin` → … →
    `stdscr.refresh()` handoff.
  - `capture_session_state` / `settle_state` lose their leading
    underscore; the dashboard will call them on
    session-end + explicit-stop respectively.

`_launch_bottle` becomes a thin orchestrator over those helpers.
No behavior change; all 453 unit tests pass and `./cli.py start
implementer --dry-run` produces identical preflight output.
didericis added 1 commit 2026-05-26 03:22:47 -04:00
feat(dashboard): agent picker modal + new-agent (n) flow
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m7s
309ffaa4ab
PRD 0020 chunk 2. Pressing `n` opens a modal that lists every
agent from the manifest with `(N running)` suffixes for ones
that already have bottles up. Type to filter (substring,
case-insensitive); j/k or arrows to navigate; Enter to confirm;
Esc clears the filter on first press, exits the picker on the
second.

On confirmation, the dashboard runs:

  - `prepare_with_preflight` from chunk 1 with curses-modal
    render + prompt callables (the preflight modal centers the
    plan summary + captures [y/N]).
  - `backend.launch(plan).__enter__()` — enters but doesn't bind
    the context to a `with`. The (cm, bottle, identity) tuple
    lands in the main loop's `bottles` dict keyed by slug.
  - `curses.endwin()` → `attach_claude(bottle)` → `stdscr.refresh()`
    handoff. The agent's claude session takes over the terminal;
    on exit the dashboard re-renders with the bottle now visible
    in the agents pane.

Crucially the context manager is held alive in `bottles` — never
`__exit__`'d at quit. Chunk 4 will wire `x` to that exit; for
now bottles started from the dashboard stay running until
explicit cleanup. Matches the PRD's "q does not tear down"
decision.

Footer surfaces `[n] new agent`. 461 unit tests pass (8 new for
`_filter_agents` and `_running_counts`).
didericis merged commit 5f2b40e679 into main 2026-05-26 03:27:02 -04:00
Sign in to join this conversation.