From 9539982d3f5167ae421e6e377f6b947677e49c5b Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 26 May 2026 00:58:34 -0400 Subject: [PATCH] docs(prd-0019): active agents in dashboard + agent-scoped edit verbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft a PRD that adds an "active agents" pane to the dashboard TUI (below the existing proposals pane) and reshapes the operator `routes edit` (e) / `pipelock edit` (p) verbs to be agent-scoped when the cursor is in the agents pane — no more global discover + disambiguation prompt on every press. Tab toggles which pane nav keys move through. Sized into 4 chunks (discovery helper → render pane → selection state → agent-scoped verbs). Six open questions called out, the biggest being whether per-bottle `compose ps` on every 1s tick scales for hosts with many bottles (answer leans toward one label-filtered `docker ps`). --- docs/prds/0019-active-agents-in-dashboard.md | 241 +++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/prds/0019-active-agents-in-dashboard.md diff --git a/docs/prds/0019-active-agents-in-dashboard.md b/docs/prds/0019-active-agents-in-dashboard.md new file mode 100644 index 0000000..6a5b8bb --- /dev/null +++ b/docs/prds/0019-active-agents-in-dashboard.md @@ -0,0 +1,241 @@ +# PRD 0019: Active agents in the dashboard, agent-scoped edit verbs + +- **Status:** Draft +- **Author:** didericis +- **Created:** 2026-05-26 + +## Summary + +The dashboard today is proposal-centric: it lists every pending +supervise tool call across every running bottle and lets the +operator approve / modify / reject from one place. The operator- +initiated `routes edit` (`e`) and `pipelock edit` (`p`) verbs are +*global* — they discover every running sidecar of that kind and +prompt for which bottle to edit if more than one is up. + +This PRD adds a first-class "active agents" view to the dashboard +and reshapes the edit verbs to be **agent-scoped**: the operator +picks an agent, then `e` / `p` (and any future per-agent verbs) +target that agent without a separate prompt. + +After this PRD the dashboard answers two questions in one screen: + + 1. What's queued for me to act on? (existing proposals view) + 2. What's currently running, and what would I act on if I + wanted to push a config edit without an agent prompt? + +## Problem + +Two rough edges in the current dashboard: + +1. **No visibility into what's actually running.** The dashboard + shows only pending proposals. If no agent has called a tool, + the screen reads "no pending proposals" — even when five + bottles are quietly working. The operator has to `docker + compose ls` (or `./cli.py cleanup -n` to see the y/N preview) + to find out what's actually live. + +2. **`e` / `p` re-discover-and-disambiguate every invocation.** + Today each press of `e` runs `discover_egress_slugs()`, finds + the running egress sidecars, and prompts if there's more than + one. The prompt interrupts the keyboard flow — and once the + operator picks a bottle, there's no carry-over to the next + edit. Editing pipelock for the same bottle right after is + another prompt. + +The proposal-centric design is fine for the "agent triggered a +remediation" case but flips the relationship the wrong way for +the "operator wants to make an unprompted change" case. + +## Goals / Success Criteria + +1. The dashboard's main screen shows two lists: pending proposals + (above) and active agents (below) — both visible at once, no + tab / mode switch. +2. Each active-agent row shows enough for the operator to + recognize the bottle at a glance: identity (slug), + agent_name (from metadata.json), started_at, and which + sidecars are up. +3. The operator can select an agent row with `j` / `k` / + arrow keys (the same nav keys already in use for proposals), + with a clear keystroke that swaps the active list (e.g., + `Tab` toggles which list `j` / `k` moves through). +4. Pressing `e` (routes edit) or `p` (pipelock edit) with an + agent selected targets that agent. No disambiguation prompt; + no global discover. +5. Pressing `e` / `p` with NO agent selected (e.g., zero agents + running, or the cursor is in the proposals pane) falls back + to today's global behavior — discover, prompt if multiple, + abort if none. +6. The active-agents list refreshes on the same ~1s tick as the + proposals list so an agent starting / stopping is reflected + without operator action. + +## Non-goals + +- **Per-agent proposal filtering.** The proposals list stays + global across bottles. Filtering ("show me only this agent's + proposals") might be a follow-up but isn't this PRD. +- **Agent lifecycle from the dashboard.** Starting / stopping + agents stays in `./cli.py start` / `./cli.py cleanup`. The + dashboard reads state; it doesn't change it. +- **Preserved-but-not-running bottles.** The active-agents list + is strictly "what's running now" (cross-referenced from + `docker compose ls`). Preserved state dirs without a live + project don't appear — `./cli.py resume ` is the + path for those. +- **A separate per-agent detail view.** The agent rows are + one-line summaries. Pressing Enter on a proposal still drops + into proposal-detail; we don't add an analogous agent-detail + screen in v1. +- **Replacing the existing `--once` mode.** `dashboard --once` + stays a proposal-only listing. No active-agents output there + (different consumers — `--once` is for scripts; the agents + view is for the interactive TUI). + +## Scope + +### In scope + +- A new "active agents" pane in the curses TUI, rendered below + the proposals pane. +- A discovery helper that returns `(slug, agent_name, + started_at, services_up)` per active compose project. Reads + agent_name + started_at from each project's `metadata.json`, + cross-references `docker compose ls` for the live list. +- Tab-toggle selection state: which pane the cursor is in. `j` + / `k` / arrow keys move within that pane. +- Rewire `_operator_edit_routes_flow` and + `_operator_edit_allowlist_flow` to accept an optional + "preselected slug". When the cursor is in the agents pane, + pass that slug; otherwise keep today's discover-and-prompt + behavior. +- Status-line indicator showing which agent is selected (or + "no agent selected" when in the proposals pane). +- Tests for the new discovery helper. + +### Out of scope + +- Changes to proposal handling (`a` / `m` / `r` / Enter all + unchanged). +- Changes to the queue-dir / supervise sidecar protocol. +- New CLI surface beyond what's in `./cli.py dashboard`. +- Touching the manifest, compose renderer, launch lifecycle. + +## Proposed design + +### Layout + +``` +claude-bottle dashboard (3 pending, 2 active) +───────────────────────────────────────────────────────── +proposals: + 03:14:22 [implementer-cy7a6] egress-block abc123… + 03:13:55 [researcher-9xqs1] pipelock-block def456… + 03:13:10 [implementer-cy7a6] capability-block ghi789… + +active agents: +> implementer-cy7a6 implementer started 02:55:01 [pipelock,egress,git-gate,supervise] + researcher-9xqs1 researcher started 02:58:14 [pipelock,supervise] + +[selected: implementer-cy7a6] q quit Tab switch j/k nav e routes p pipelock a/m/r/Enter +``` + +- One screen, two lists. Header counts both totals. +- A `>` cursor and reverse-video highlight mark the currently + selected row in the active pane. +- Status footer carries `[selected: ]` (or `[no agent + selected]`) so it's always clear what `e` / `p` will target. + +### Selection model + +- `Tab` (or Shift-Tab) toggles which pane `j` / `k` / + arrow keys move through. +- Each pane keeps its own selection index. Switching panes + doesn't lose the position in the other. +- `e` / `p`: + - Cursor in the agents pane → use that agent's slug. + - Cursor in the proposals pane → today's discover-and-prompt + behavior (so the muscle memory still works for operators + who didn't switch panes first). + +### Active-agent discovery + +A new helper `discover_active_agents()` in dashboard.py +returns a list of `ActiveAgent(slug, agent_name, started_at, +services)`: + + 1. `list_active_slugs()` (already in + `backend/docker/compose.py`) → list of slugs. + 2. For each slug: read `state//metadata.json` → + `agent_name`, `started_at`. + 3. For each slug: `docker compose -p ps --format + json` → set of running service names. + +Step 3 is the part that's per-bottle and could be slow on +hosts with many bottles. Open question below. + +### Implementation chunks + +Sized small. + +1. **Discovery helper + dataclass.** Pure-ish: takes + `list_active_slugs()` as injected, reads metadata + queries + compose ps. Unit-test with mocked subprocess. No UI yet. +2. **Render the agents pane.** Wire `discover_active_agents` + into `_main_loop`'s tick, render below proposals, no + selection model yet (cursor stays in proposals). +3. **Selection state + Tab toggle.** Add the `which_pane` + variable, route `j/k/arrow` based on it, status footer. +4. **Agent-scoped `e` / `p`.** Pass selected slug into the + edit flows when the agents pane is focused; keep today's + global behavior when the proposals pane is focused. + +## Open questions + +1. **`compose ps` per bottle: too slow?** On a host with + 10+ active bottles, calling `docker compose -p ps` per + project on every 1s tick is 10+ subprocess calls per + second. Options: (a) cache the services list and refresh + on a slower cadence (e.g., every 5s); (b) skip the + per-bottle services column and just show the slug + agent + name; (c) one `docker ps --filter label=...` call that + buckets containers by `com.docker.compose.project` label. + Probably (c) — one call, no per-bottle fanout. + +2. **What if `metadata.json` is missing or stale?** For a + bottle started by pre-chunk-3 code (no `compose_project` + field), or a state dir written by a tool we don't know + about, the metadata read can fail. Render with + `agent_name = ?` rather than dropping the row. + +3. **Selection persistence across refresh ticks.** If the + currently-selected agent is no longer running (it exited + between ticks), the selection should fall back to the + previous row, not jump to the top. Mirrors the existing + proposals-list behavior. + +4. **Should `e` / `p` still prompt when the cursor is in the + proposals pane but only one agent is running?** Today the + single-bottle case skips the prompt; we'd keep that. Two + or more bottles still prompts in proposals-pane mode. + +5. **Color / highlight for the selected agent.** The proposals + pane uses green for newly-arrived. Agents could use a + different attribute (e.g., reverse video for selection, + no color for the row itself). Aesthetic decision; pick + something readable in the standard 8-color palette. + +6. **Selecting a proposal cross-selects its agent?** Possible + UX: highlighting a proposal in the proposals pane could + auto-move the agents-pane cursor to that proposal's + bottle. Cute, but probably confusing — the explicit Tab + toggle is clearer. Out of v1. + +## References + +- PRD 0013 — supervise sidecar (proposals + queue) +- PRD 0014 / 0015 / 0016 — the apply flows the edit verbs + drive +- PRD 0018 — compose-per-instance; `list_active_slugs` + + metadata.json source-of-truth