docs(prd-0021): rewrite as standalone — no references to closed PR #48
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.
This commit is contained in:
@@ -10,55 +10,34 @@ When the dashboard runs inside tmux, lay it out as the **left
|
|||||||
pane** of a two-pane window with the **selected agent's claude
|
pane** of a two-pane window with the **selected agent's claude
|
||||||
session in the right pane**. Pressing Enter on an agent in the
|
session in the right pane**. Pressing Enter on an agent in the
|
||||||
agents pane swaps the right pane to that agent's session
|
agents pane swaps the right pane to that agent's session
|
||||||
(respawning with `claude --continue` so the conversation
|
(respawning with `claude --continue` so the conversation picks
|
||||||
picks up where it left off). Pressing `n` to start a new agent
|
up where it left off). Pressing `n` to start a new agent spawns
|
||||||
spawns it directly into the right pane. The dashboard is the
|
it directly into the right pane. The dashboard is the
|
||||||
operator's persistent left-hand surface; claude is always
|
operator's persistent left-hand surface; claude is always
|
||||||
visible to its right.
|
visible to its right.
|
||||||
|
|
||||||
Outside tmux, fall back to today's handoff (`curses.endwin` →
|
Outside tmux, fall back to today's handoff (`curses.endwin →
|
||||||
foreground claude → `stdscr.refresh`). The split-pane UX is
|
foreground claude → stdscr.refresh`). The split-pane UX is
|
||||||
opt-in by environment — running the dashboard inside a tmux
|
opt-in by environment — running the dashboard inside a tmux
|
||||||
session enables it, no flag required.
|
session enables it, no flag required.
|
||||||
|
|
||||||
This supersedes the "new-window per agent" shape of PR #48
|
|
||||||
(option 3 from the research doc) with a tighter,
|
|
||||||
selection-driven model where the right pane reflects what the
|
|
||||||
operator is *currently focused on* rather than accumulating one
|
|
||||||
window per ever-attached bottle.
|
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
PR #48 (and option 3 from `docs/research/claude-code-pane-in-
|
The dashboard's "Enter to attach" (PRD 0020 chunk 3) and its
|
||||||
dashboard.md`) gets us partway there: when `$TMUX` is set, the
|
new-agent `n` flow both take over the terminal for the
|
||||||
dashboard's Enter / `n` keys spawn claude in a new tmux
|
duration of the claude session via `curses.endwin`. While
|
||||||
**window** (separate tab) instead of taking over the terminal.
|
claude has the screen, the dashboard's proposal queue and
|
||||||
That's better than the handoff in two respects — the dashboard
|
agents pane are invisible. Any tool call that lands during a
|
||||||
keeps refreshing, and multiple agents get their own panes — but
|
claude session is silent until the operator exits back to the
|
||||||
it has three real rough edges:
|
dashboard.
|
||||||
|
|
||||||
1. **No simultaneous view.** A tmux window is a separate tab.
|
The split-pane shape this PRD proposes keeps both surfaces
|
||||||
The operator switches to claude via `C-b n`, away from the
|
visible: dashboard always on the left, the currently-selected
|
||||||
dashboard. Watching proposals queue while talking to claude
|
agent on the right. The dashboard's existing selection model
|
||||||
is back to the two-terminals-side-by-side workflow the
|
(PRD 0019) drives which agent occupies the right pane —
|
||||||
dashboard was supposed to collapse.
|
moving the cursor in the agents pane is a no-op; pressing
|
||||||
|
Enter swaps the right pane to that agent's session. One
|
||||||
2. **Window accumulation.** Each agent ever attached adds a
|
window, two panes, no terminal handoff.
|
||||||
tmux window. After a day with 10 bottles the operator has 10
|
|
||||||
windows to navigate; the dashboard's agents pane and tmux's
|
|
||||||
window list duplicate each other.
|
|
||||||
|
|
||||||
3. **Selection doesn't drive layout.** The dashboard's
|
|
||||||
selection model (PRD 0019) already knows which agent the
|
|
||||||
operator cares about right now. The new-window shape ignores
|
|
||||||
that — the operator has to manually switch tmux windows
|
|
||||||
independent of the dashboard's cursor position.
|
|
||||||
|
|
||||||
The split-pane shape this PRD proposes uses the same selection
|
|
||||||
the dashboard already tracks: cursor on agent X + Enter →
|
|
||||||
right pane shows agent X. Move cursor to agent Y + Enter →
|
|
||||||
right pane respawns with agent Y. One window, two panes, no
|
|
||||||
accumulation.
|
|
||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
@@ -73,72 +52,73 @@ accumulation.
|
|||||||
3. Pressing `n` to start a new agent (the existing chunk-2 flow
|
3. Pressing `n` to start a new agent (the existing chunk-2 flow
|
||||||
from PRD 0020) directs the spawned claude session into the
|
from PRD 0020) directs the spawned claude session into the
|
||||||
right pane instead of taking over the terminal.
|
right pane instead of taking over the terminal.
|
||||||
4. Pressing `x` to stop a dashboard-owned bottle (PRD 0020
|
4. Pressing `x` to stop a dashboard-owned bottle: if that
|
||||||
chunk 4): if that bottle was the right-pane occupant, the
|
bottle was the right-pane occupant, the right pane is
|
||||||
right pane is cleared (or shows a brief "stopped" message).
|
cleared (or shows a brief "stopped" message).
|
||||||
5. Closing the right pane manually via tmux (e.g., `C-b x`)
|
5. Closing the right pane manually via tmux (e.g., `C-b x`)
|
||||||
leaves the dashboard intact; the next Enter creates a fresh
|
leaves the dashboard intact; the next Enter creates a fresh
|
||||||
right pane.
|
right pane.
|
||||||
6. Outside tmux (`$TMUX` unset), the dashboard's Enter / `n`
|
6. Outside tmux (`$TMUX` unset), the dashboard's Enter / `n`
|
||||||
behavior falls back to today's handoff. No tmux dependency
|
behavior falls back to today's handoff. No tmux dependency
|
||||||
for non-tmux users.
|
for non-tmux users.
|
||||||
7. The two-pane layout works whether the operator pre-arranges
|
|
||||||
tmux themselves or lets the dashboard set it up. Both paths
|
|
||||||
land at the same place.
|
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- **The embedded-emulator option (option 2 from the research
|
- **The embedded-emulator option** from
|
||||||
doc).** This PRD stays on the multiplexer-delegated shape.
|
`docs/research/claude-code-pane-in-dashboard.md`. This PRD
|
||||||
pyte-driven in-curses rendering is a separate, much larger
|
stays on the multiplexer-delegated shape. pyte-driven
|
||||||
decision.
|
in-curses rendering is a separate, much larger decision.
|
||||||
- **Multi-pane / grid layout.** No "show 4 agents at once."
|
- **Multi-pane / grid layout.** No "show 4 agents at once."
|
||||||
One left, one right. Picking that as a constraint dramatically
|
One left, one right. Picking that as a constraint
|
||||||
simplifies the state machine.
|
dramatically simplifies the state machine.
|
||||||
- **Persistence across dashboard exits.** The tmux session
|
- **Persistence across dashboard exits.** The tmux session
|
||||||
state (which agent is in the right pane) doesn't survive a
|
state (which agent is in the right pane) doesn't survive a
|
||||||
dashboard restart. The bottles themselves persist (PRD 0020's
|
dashboard restart. The bottles themselves persist (PRD 0020's
|
||||||
`q`-doesn't-tear-down), but reattaching is one Enter away
|
`q`-doesn't-tear-down); reattaching is one Enter away after a
|
||||||
after a fresh launch.
|
fresh launch.
|
||||||
- **Replacing PR #48 entirely while it's in flight.** This PRD
|
|
||||||
ships AFTER PR #48 lands (or merges over it). The shared
|
|
||||||
helpers (`_attach_via_tmux`, `_build_tmux_attach_argv`) carry
|
|
||||||
forward; the dispatch logic changes from `new-window` to
|
|
||||||
`split-window` / `respawn-pane`.
|
|
||||||
- **Cross-tmux-session orchestration.** The dashboard owns
|
- **Cross-tmux-session orchestration.** The dashboard owns
|
||||||
panes in its own session. Other tmux sessions on the same
|
panes in its own session. Other tmux sessions on the same
|
||||||
host are untouched.
|
host are untouched.
|
||||||
- **A "right pane is detached" mode** where claude runs in a
|
- **A "right pane is detached" mode** where claude runs in a
|
||||||
pane that's not visible until expanded. Out of v1.
|
pane that's not visible until expanded. Out of v1.
|
||||||
|
- **Auto-execing into a fresh tmux session** when launched
|
||||||
|
outside one. See open question #3.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
### In scope
|
### In scope
|
||||||
|
|
||||||
- A `_tmux_split_pane_layout()` helper that, on first attach,
|
- A `_in_tmux()` helper that returns True when `$TMUX` is set,
|
||||||
runs `tmux split-window -h …` to create the right pane and
|
with `FileNotFoundError`-safe `subprocess.run` calls
|
||||||
remembers its tmux pane id so subsequent attaches use
|
around every tmux invocation so a missing tmux binary
|
||||||
`tmux respawn-pane -t <id> …` to swap content.
|
cleanly falls back to handoff mode.
|
||||||
- Pane-id state on the main loop: `right_pane_id: str | None`.
|
- Two new state fields on the main loop: `right_pane_id: str |
|
||||||
None means "no right pane yet" → next attach creates one
|
None` (the tmux pane id we created — None = "no right pane
|
||||||
via split-window. Non-None → respawn-pane.
|
yet, next attach must split") and `right_pane_slug: str |
|
||||||
- A `_pane_exists(pane_id)` check via `tmux list-panes`
|
None` (which bottle is currently in the right pane, for
|
||||||
before respawn, so a manually-closed right pane gracefully
|
the stop + indicator hooks).
|
||||||
falls back to a new split.
|
- A `_tmux_split_pane_create(bottle, *, resume) -> str | None`
|
||||||
- Updated dispatch in `_attach_to_bottle` and `_new_agent_flow`:
|
helper that runs `tmux split-window -h -P -F '#{pane_id}' …`
|
||||||
- `$TMUX` set → split-pane / respawn-pane path
|
and returns the new pane id, or None on failure.
|
||||||
- `$TMUX` unset → existing handoff
|
- A `_tmux_respawn_pane(pane_id, bottle, *, resume) -> bool`
|
||||||
- PR #48's `tmux new-window` path becomes the fallback inside
|
helper that runs `tmux respawn-pane -k -t <id> …` and
|
||||||
the split-pane code (e.g., if `tmux split-window` fails, or
|
returns success.
|
||||||
via a config knob).
|
- A `_tmux_pane_exists(pane_id) -> bool` check via
|
||||||
- `_stop_bottle_flow` clears `right_pane_id` if the stopped
|
`tmux list-panes -F '#{pane_id}'` so a manually-closed right
|
||||||
bottle was the right-pane occupant.
|
pane gracefully falls back to a new split.
|
||||||
|
- Modified `_attach_to_bottle` and `_new_agent_flow` (PRD
|
||||||
|
0020) to dispatch through the tmux helpers when `$TMUX` is
|
||||||
|
set, falling back to the existing handoff otherwise.
|
||||||
|
- `_stop_bottle_flow` updated to clear `right_pane_slug` when
|
||||||
|
the stopped bottle matches.
|
||||||
|
- An indicator in the agents pane (e.g., `*` prefix or
|
||||||
|
alternate attr) marking the row whose bottle is currently
|
||||||
|
in the right pane.
|
||||||
|
|
||||||
### Out of scope
|
### Out of scope
|
||||||
|
|
||||||
- Choosing the split direction (-h vs -v). The PRD assumes
|
- Choosing the split direction (-h vs -v) as a runtime knob.
|
||||||
horizontal split (left/right). Open question if the operator
|
v1 is `-h` (left / right). See open question #1.
|
||||||
wants vertical.
|
|
||||||
- Sizing the split (e.g., 40/60 vs 50/50). v1 takes the tmux
|
- Sizing the split (e.g., 40/60 vs 50/50). v1 takes the tmux
|
||||||
default; sizing knob deferred.
|
default; sizing knob deferred.
|
||||||
- Persisting the right-pane occupant slug to disk so a fresh
|
- Persisting the right-pane occupant slug to disk so a fresh
|
||||||
@@ -148,134 +128,170 @@ accumulation.
|
|||||||
|
|
||||||
### State machine
|
### State machine
|
||||||
|
|
||||||
|
Two new fields on the main loop:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# new state on _main_loop:
|
right_pane_id: str | None = None # tmux pane id, or None
|
||||||
right_pane_id: str | None = None # tmux pane id we created
|
right_pane_slug: str | None = None # current occupant, or None
|
||||||
right_pane_slug: str | None = None # which bottle's session is in it
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dispatch logic:
|
### Dispatch
|
||||||
|
|
||||||
|
`_attach_to_bottle` and `_new_agent_flow` (from PRD 0020) gain
|
||||||
|
a tmux branch:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def _spawn_into_right_pane(bottle, slug, *, resume) -> str:
|
def _attach_in_tmux(bottle, slug, *, resume) -> str:
|
||||||
if not _in_tmux():
|
|
||||||
# Fall back to today's handoff.
|
|
||||||
return _attach_via_handoff(stdscr, bottle, slug, resume=resume)
|
|
||||||
|
|
||||||
nonlocal right_pane_id, right_pane_slug
|
nonlocal right_pane_id, right_pane_slug
|
||||||
if right_pane_id and _pane_exists(right_pane_id):
|
|
||||||
# Respawn content in the existing pane.
|
# If we have a remembered pane and it still exists, respawn it.
|
||||||
ok = _tmux_respawn_pane(right_pane_id, bottle, resume=resume)
|
if right_pane_id and _tmux_pane_exists(right_pane_id):
|
||||||
if not ok:
|
if _tmux_respawn_pane(right_pane_id, bottle, resume=resume):
|
||||||
right_pane_id = None # fall through to create
|
right_pane_slug = slug
|
||||||
if right_pane_id is None or not _pane_exists(right_pane_id):
|
return f"[{slug}] in right pane"
|
||||||
right_pane_id = _tmux_split_pane_create(bottle, resume=resume)
|
right_pane_id = None # respawn failed; fall through
|
||||||
if right_pane_id is None:
|
|
||||||
|
# Otherwise create a fresh split.
|
||||||
|
pane_id = _tmux_split_pane_create(bottle, resume=resume)
|
||||||
|
if pane_id is None:
|
||||||
# tmux failed; fall back to handoff.
|
# tmux failed; fall back to handoff.
|
||||||
return _attach_via_handoff(stdscr, bottle, slug, resume=resume)
|
return _attach_via_handoff(stdscr, bottle, slug, resume=resume)
|
||||||
|
right_pane_id = pane_id
|
||||||
right_pane_slug = slug
|
right_pane_slug = slug
|
||||||
return f"[{slug}] in right pane"
|
return f"[{slug}] in right pane"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pane creation + respawn
|
The non-tmux path is unchanged from PRD 0020 — `_attach_via_
|
||||||
|
handoff` is what those two flows already do today (curses.
|
||||||
|
endwin → attach_claude → stdscr.refresh).
|
||||||
|
|
||||||
|
### Pane creation
|
||||||
|
|
||||||
|
`tmux split-window -h -P -F '#{pane_id}'` opens a right pane
|
||||||
|
and prints its pane id (e.g., `%12`). The `-h` is horizontal
|
||||||
|
split (left/right); the `-P -F` combo emits the new pane's
|
||||||
|
identifier for tracking.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def _tmux_split_pane_create(bottle, *, resume) -> str | None:
|
def _tmux_split_pane_create(bottle, *, resume) -> str | None:
|
||||||
"""Open a right pane via `tmux split-window -h -P -F '#{pane_id}'`.
|
|
||||||
The -P -F combo prints the new pane's id to stdout so we
|
|
||||||
can track it for respawn."""
|
|
||||||
docker_argv = bottle.claude_docker_argv(_claude_args(resume=resume))
|
docker_argv = bottle.claude_docker_argv(_claude_args(resume=resume))
|
||||||
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["tmux", "split-window", "-h",
|
["tmux", "split-window", "-h",
|
||||||
"-P", "-F", "#{pane_id}",
|
"-P", "-F", "#{pane_id}",
|
||||||
*docker_argv],
|
*docker_argv],
|
||||||
capture_output=True, text=True, check=False,
|
capture_output=True, text=True, check=False,
|
||||||
)
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return None
|
return None
|
||||||
return result.stdout.strip()
|
return result.stdout.strip() or None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pane respawn
|
||||||
|
|
||||||
|
`tmux respawn-pane -k -t <id> …` replaces the pane's content
|
||||||
|
without re-splitting. `-k` kills the existing process first
|
||||||
|
(claude session ends; the underlying bottle is untouched).
|
||||||
|
`--continue` on the new claude invocation picks up the prior
|
||||||
|
conversation.
|
||||||
|
|
||||||
|
```python
|
||||||
def _tmux_respawn_pane(pane_id, bottle, *, resume) -> bool:
|
def _tmux_respawn_pane(pane_id, bottle, *, resume) -> bool:
|
||||||
"""Replace the pane's content via `tmux respawn-pane -t <id> -k …`.
|
|
||||||
-k kills the existing process before respawning. Idempotent
|
|
||||||
on a pane that's already empty."""
|
|
||||||
docker_argv = bottle.claude_docker_argv(_claude_args(resume=resume))
|
docker_argv = bottle.claude_docker_argv(_claude_args(resume=resume))
|
||||||
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["tmux", "respawn-pane", "-k", "-t", pane_id, *docker_argv],
|
["tmux", "respawn-pane", "-k", "-t", pane_id, *docker_argv],
|
||||||
capture_output=True, text=True, check=False,
|
capture_output=True, text=True, check=False,
|
||||||
)
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pane-existence check
|
### Pane-existence check
|
||||||
|
|
||||||
`tmux list-panes -F '#{pane_id}'` lists pane ids in the current
|
```python
|
||||||
window. The dashboard checks for `right_pane_id` membership
|
def _tmux_pane_exists(pane_id) -> bool:
|
||||||
before issuing respawn-pane; if absent (operator closed the
|
try:
|
||||||
pane via `C-b x`), it falls back to creating a fresh split.
|
result = subprocess.run(
|
||||||
|
["tmux", "list-panes", "-F", "#{pane_id}"],
|
||||||
|
capture_output=True, text=True, check=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False
|
||||||
|
return pane_id in result.stdout.splitlines()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bottle.claude_docker_argv
|
||||||
|
|
||||||
|
The tmux helpers need the full docker-exec argv for claude —
|
||||||
|
specifically including the `--append-system-prompt-file <path>`
|
||||||
|
flag that `DockerBottle.exec_claude` appends today when the
|
||||||
|
bottle has a prompt path. Refactor: split `exec_claude` into a
|
||||||
|
pure `claude_docker_argv(args, *, tty)` that returns the argv
|
||||||
|
and a thin `exec_claude` that calls `subprocess.run` on it.
|
||||||
|
Both the tmux path AND the existing foreground path use the
|
||||||
|
same argv builder.
|
||||||
|
|
||||||
### Stop integration
|
### Stop integration
|
||||||
|
|
||||||
`_stop_bottle_flow` already pops the bottle from the
|
`_stop_bottle_flow` (from PRD 0020 chunk 4) gets a small
|
||||||
`bottles` dict and calls `cm.__exit__`. The new bit: if the
|
addition: after the bottle is torn down, if `right_pane_slug
|
||||||
stopped slug matches `right_pane_slug`, clear both pane
|
== stopped_slug`, clear it. The tmux pane itself stays open
|
||||||
tracking variables. The tmux pane itself stays open with
|
with claude's "container not found" error showing. The next
|
||||||
docker exec's "container not found" error showing — acceptable;
|
Enter on a different agent will respawn it.
|
||||||
the operator hits Enter on a different agent to repurpose it
|
|
||||||
(respawn-pane will replace the broken state).
|
### Indicator in agents pane
|
||||||
|
|
||||||
|
When `right_pane_slug` matches an agent row's slug, render
|
||||||
|
that row with a `*` prefix (and / or reverse-video, if the
|
||||||
|
focus indicator doesn't already conflict). Gives the operator
|
||||||
|
a clear visual link between dashboard selection and right-
|
||||||
|
pane content.
|
||||||
|
|
||||||
### Fallback when tmux fails
|
### Fallback when tmux fails
|
||||||
|
|
||||||
Three failure modes worth handling:
|
Three failure modes worth handling:
|
||||||
|
|
||||||
1. **`$TMUX` set but tmux binary not on PATH** (rare but
|
1. **`$TMUX` set but tmux binary not on PATH.** `subprocess.run`
|
||||||
possible in nested containers). Detected when
|
raises `FileNotFoundError` — treat as not-in-tmux, fall back
|
||||||
`subprocess.run` raises `FileNotFoundError` — treat as not-
|
to handoff. Should be rare but happens in nested
|
||||||
in-tmux, fall back to handoff.
|
containers.
|
||||||
2. **`tmux split-window` returns non-zero** (e.g., pane size
|
2. **`tmux split-window` / `respawn-pane` returns non-zero.**
|
||||||
too small, server lost). Surface a status-line error and
|
Surface a status-line error and fall back to handoff for
|
||||||
fall back to handoff for that one keypress.
|
that one keypress. The dashboard stays usable.
|
||||||
3. **Right pane manually closed.** Detected at next attach via
|
3. **Right pane manually closed.** Detected at next attach via
|
||||||
pane-exists check; create fresh split.
|
`_tmux_pane_exists`; create a fresh split.
|
||||||
|
|
||||||
### Interaction with PR #48
|
|
||||||
|
|
||||||
PR #48 introduced `_in_tmux()`, `_attach_via_tmux` (using
|
|
||||||
`tmux new-window`), and `_build_tmux_attach_argv`. This PRD
|
|
||||||
reuses the first; rewrites the second; the third (window
|
|
||||||
argv builder) becomes one of several tmux-argv builders this
|
|
||||||
PRD introduces. Net change: PR #48's new-window behavior is
|
|
||||||
replaced by split-pane behavior. The diff is moderate but
|
|
||||||
contained — the abstraction shape carries through.
|
|
||||||
|
|
||||||
## Implementation chunks
|
## Implementation chunks
|
||||||
|
|
||||||
Sized small.
|
Sized small.
|
||||||
|
|
||||||
1. **Pane state + create path.** Add `right_pane_id` /
|
1. **`claude_docker_argv` refactor.** Pure-ish split of
|
||||||
`right_pane_slug` to the main loop; implement
|
`DockerBottle.exec_claude` so both foreground and tmux
|
||||||
`_tmux_split_pane_create`; wire Enter / `n` to use it when
|
paths build on the same argv. No behavior change for the
|
||||||
`right_pane_id is None`. No respawn yet — second attach
|
existing tests.
|
||||||
creates a *second* split-pane (visibly wrong, but isolated).
|
2. **tmux helpers + pane state.** Add `_in_tmux`,
|
||||||
2. **Respawn for subsequent attaches.** Implement
|
`_tmux_split_pane_create`, `_tmux_respawn_pane`,
|
||||||
`_tmux_respawn_pane` + `_pane_exists`; route second-and-
|
`_tmux_pane_exists`, plus the two new state fields on the
|
||||||
subsequent attaches through respawn instead of split. The
|
main loop. Wire `_attach_to_bottle` to dispatch through
|
||||||
"wrong" behavior from chunk 1 disappears.
|
tmux when in tmux, fall back to handoff otherwise.
|
||||||
3. **Stop integration.** `_stop_bottle_flow` clears
|
3. **New-agent flow integration.** `_new_agent_flow` (PRD
|
||||||
`right_pane_slug` when the stopped bottle matches; tests.
|
0020 chunk 2) gains the same tmux dispatch — first attach
|
||||||
4. **Replace PR #48's new-window with split-pane.** Remove the
|
on a freshly-started agent goes into the right pane.
|
||||||
`tmux new-window` invocation; keep `_build_tmux_attach_argv`
|
4. **Stop integration + right-pane indicator.**
|
||||||
as a shared helper if useful. Update PR #48's tests to
|
`_stop_bottle_flow` clears `right_pane_slug` when matched;
|
||||||
match the new behavior. (If PR #48 has merged by then, this
|
`_format_agent_row` (PRD 0019 chunk 2) gets a `*` prefix
|
||||||
is a follow-up commit; if not, the two PRs reconcile at
|
when its slug matches `right_pane_slug`.
|
||||||
merge time.)
|
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
1. **Split direction: horizontal (`-h`) vs vertical (`-v`).**
|
1. **Split direction: horizontal (`-h`) vs vertical (`-v`).**
|
||||||
The PRD's "left / right" framing implies `-h`. But on a
|
The PRD's "left / right" framing implies `-h`. On a
|
||||||
short-and-wide terminal a vertical (top / bottom) split
|
short-and-wide terminal a vertical (top / bottom) split
|
||||||
might give the dashboard more vertical real estate. Pick
|
might give the dashboard more vertical real estate. Pick
|
||||||
`-h` for v1; revisit if the dashboard's agents-pane row
|
`-h` for v1; revisit if the dashboard's agents-pane row
|
||||||
@@ -292,10 +308,10 @@ Sized small.
|
|||||||
3. **Dashboard launched OUTSIDE tmux but tmux is installed.**
|
3. **Dashboard launched OUTSIDE tmux but tmux is installed.**
|
||||||
Should the dashboard auto-exec itself inside a fresh tmux
|
Should the dashboard auto-exec itself inside a fresh tmux
|
||||||
session to get the split-pane experience? Convenient but
|
session to get the split-pane experience? Convenient but
|
||||||
surprising (`./cli.py dashboard` shouldn't silently change
|
surprising (`./cli.py dashboard` shouldn't silently
|
||||||
what session you're in). v1 leaves this off — operators
|
change what session you're in). v1 leaves this off —
|
||||||
who want split-pane mode start tmux themselves and then
|
operators who want split-pane mode start tmux themselves
|
||||||
run the dashboard.
|
and then run the dashboard.
|
||||||
|
|
||||||
4. **Pane size after split.** tmux's default for `split-window
|
4. **Pane size after split.** tmux's default for `split-window
|
||||||
-h` is 50/50. The dashboard's column widths are designed
|
-h` is 50/50. The dashboard's column widths are designed
|
||||||
@@ -304,26 +320,26 @@ Sized small.
|
|||||||
want ~50 cols and claude gets the rest. Add `-p <pct>` or
|
want ~50 cols and claude gets the rest. Add `-p <pct>` or
|
||||||
`-l <cols>` flag? Open question; v1 uses tmux's default.
|
`-l <cols>` flag? Open question; v1 uses tmux's default.
|
||||||
|
|
||||||
5. **Multi-window tmux sessions.** If the operator has the
|
5. **Cursor highlighting in the agents pane to indicate "this
|
||||||
dashboard in tmux window A and unrelated work in window B,
|
one's in the right pane right now."** Resolved YES in
|
||||||
the split-pane goes into window A as expected (tmux
|
scope — `*` prefix on the matching row. Open: should it
|
||||||
commands are window-scoped by `$TMUX_PANE` context). No
|
also use color, and which color doesn't conflict with the
|
||||||
special handling needed — but worth verifying behavior
|
PRD-0019 focus indicator?
|
||||||
when the operator switches away from window A before the
|
|
||||||
dashboard issues a split.
|
|
||||||
|
|
||||||
6. **Cursor highlighting in the agents pane to indicate "this
|
6. **Concurrent dashboards in different tmux windows.**
|
||||||
one's in the right pane right now."** The dashboard could
|
Multiple `./cli.py dashboard` invocations in different
|
||||||
mark the slug currently occupying the right pane with a
|
tmux windows would each create their own right pane —
|
||||||
`*` prefix or alternate color in the agents pane. Probably
|
probably fine, each has its own state, but worth
|
||||||
yes for v1 — operator clarity. The state's already
|
verifying that `tmux list-panes` is scoped to the right
|
||||||
tracked (`right_pane_slug`).
|
window context.
|
||||||
|
|
||||||
7. **What about the proposals pane?** Approvals (`a`/`m`/`r`)
|
7. **`$TMUX_PANE` vs `$TMUX`.** Tmux sets both. We only check
|
||||||
are unchanged — they don't open new panes, they apply
|
`$TMUX` (presence of the server socket); `$TMUX_PANE` is
|
||||||
in-process. The proposals pane stays exactly as today,
|
the dashboard's own pane id, which we could capture to be
|
||||||
just narrower because it shares the left pane with the
|
more precise about `tmux split-window -t <pane>` (split
|
||||||
agents list.
|
relative to the dashboard's pane specifically). Probably
|
||||||
|
not needed since `split-window` defaults to the current
|
||||||
|
active pane, but worth verifying.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
@@ -333,7 +349,6 @@ Sized small.
|
|||||||
(`_attach_to_bottle`, `_new_agent_flow`, `_stop_bottle_flow`
|
(`_attach_to_bottle`, `_new_agent_flow`, `_stop_bottle_flow`
|
||||||
— the three hooks this PRD changes)
|
— the three hooks this PRD changes)
|
||||||
- `docs/research/claude-code-pane-in-dashboard.md` — the
|
- `docs/research/claude-code-pane-in-dashboard.md` — the
|
||||||
three-option survey; this PRD picks option 3 with a tighter
|
three-option survey of how to surface claude inside the
|
||||||
|
dashboard; this PRD picks option 3 with a tighter
|
||||||
split-pane variant
|
split-pane variant
|
||||||
- PR #48 — opt-in tmux via `tmux new-window`; this PRD
|
|
||||||
supersedes its window-mode with pane-mode
|
|
||||||
|
|||||||
Reference in New Issue
Block a user