From 39e0976ace89583b3b95655aaee61038bf30bb42 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 7 Jun 2026 12:01:11 -0400 Subject: [PATCH] docs(prd): redesign label+color prompt as a curses modal window - Single modal with two steps (label then color) instead of bare text prompts dropped to terminal - Default label is -; first keystroke replaces the pre-fill rather than appending to it - Color step shows a navigable list with live color preview; (none) selected by default; Esc skips - Modal lives in tui.py and is shared between supervisor flow and cmd_start Co-Authored-By: Claude Sonnet 4.6 --- docs/prds/prd-new-named-labelled-agents.md | 213 ++++++++++++--------- 1 file changed, 119 insertions(+), 94 deletions(-) diff --git a/docs/prds/prd-new-named-labelled-agents.md b/docs/prds/prd-new-named-labelled-agents.md index b5453ad..b15183b 100644 --- a/docs/prds/prd-new-named-labelled-agents.md +++ b/docs/prds/prd-new-named-labelled-agents.md @@ -7,17 +7,19 @@ ## Summary -At agent launch time, prompt the operator for a short human-readable label -(defaulting to the manifest agent key) and an optional color from the 16-color -ANSI palette. Store both in the bottle's `metadata.json`. Display the label — -rendered in the chosen color — in the dashboard's active-agents pane, replacing -the bare manifest key. Inject the label and color into the in-container -`claude.json` as `name` / `color` so Claude Code can surface them in its own -harness when upstream support lands. +At agent launch time, present the operator with a curses modal to optionally +set a human-readable label and color for the agent before it launches. The +modal pre-fills the label with the current agent name pattern (e.g. +`implementer-a3f9`) and leaves color unset; Enter with no changes accepts +those defaults. Store both in the bottle's `metadata.json`. Display the label — +rendered in the chosen color — in the supervisor's active-agents pane, +replacing the bare manifest key. Inject the label and color into the +in-container `claude.json` as `name` / `color` so Claude Code can surface them +in its own harness when upstream support lands. ## Problem -The dashboard's agents pane identifies each running instance by its manifest +The supervisor's agents pane identifies each running instance by its manifest agent key (e.g., `implementer`) plus a random slug suffix. When an operator runs three `implementer` bottles simultaneously — one each for three different repos — the pane shows: @@ -35,33 +37,34 @@ which breaks the moment they switch windows. ## Goals / Success Criteria -1. After the operator selects an agent name (dashboard picker or CLI argument), - they are prompted for a label. The prompt suggests the manifest key as the - default; pressing Enter (or providing no input) accepts it. The label may - contain any printable characters up to 64 bytes. -2. After the label prompt, the operator is optionally prompted for a color from - the 16-color ANSI palette (names: `black`, `red`, `green`, `yellow`, `blue`, - `magenta`, `cyan`, `white`, `bright-black`, `bright-red`, `bright-green`, - `bright-yellow`, `bright-blue`, `bright-magenta`, `bright-cyan`, - `bright-white`). Pressing Enter without a selection skips color entirely. -3. `label` and `color` are stored in `BottleMetadata` and written to the +1. After the operator selects an agent (supervisor picker or CLI argument), a + curses modal appears before the backend picker / preflight. The modal + pre-fills the label with `-` (the same pattern + currently shown in the agents pane). No color is pre-selected. +2. In the modal, any printable keystroke immediately replaces the pre-filled + label and starts building the new name. Backspace edits normally. Enter + at any point confirms — accepting the pre-fill if nothing was typed, or + the in-progress text otherwise. +3. After the label field is confirmed, the modal presents color selection: + a list of the 16 ANSI color names the operator can navigate with arrow + keys, or Enter with no selection to skip color entirely. +4. `label` and `color` are stored in `BottleMetadata` and written to the bottle's `metadata.json`. Both fields default to `""` (empty / unset). -4. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them +5. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them from `metadata.json`. -5. `_format_agent_row` uses the label when non-empty (falling back to - `agent_name`). If a non-empty color is set and the terminal supports it, the - label substring is rendered in that color. -6. `BottleSpec` carries `label` and `color`; the docker backend's `prepare` - step copies them into `BottleMetadata`. -7. `ClaudeAgentProvider.provision_plan()` in - `bot_bottle/contrib/claude/agent_provider.py` writes `label` → `"name"` and - `color` → `"color"` into the generated `claude.json`, alongside the existing - fields. Fields are omitted when empty. -8. The dashboard's `_new_agent_flow` (PRD 0020) includes the label+color step - between agent selection and the backend picker. -9. `cmd_start` (CLI) includes the label+color step after argument validation - and before prepare-with-preflight. -10. All existing unit tests stay green; no new tests are required for this +6. The supervisor's agent row uses the label when non-empty (falling back to + `agent_name`). If a non-empty color is set and the terminal supports it, + the label substring is rendered in that color. +7. `BottleSpec` carries `label` and `color`; both backends' `prepare` steps + copy them into `BottleMetadata`. +8. `ClaudeAgentProvider.provision_plan()` writes `label` → `"name"` and + `color` → `"color"` into the generated `claude.json`. Fields are omitted + when empty. +9. The supervisor's `_new_agent_flow` includes the modal between agent + selection and the backend picker. +10. `cmd_start` (CLI) shows the same modal (via the shared `tui` module) + before `_launch_bottle`; passes `label` / `color` into `BottleSpec`. +11. All existing unit tests stay green; no new tests are required for this change (the label/color fields are thin plumbing with no branching logic worth unit-testing beyond the already-tested metadata read/write path). @@ -70,37 +73,36 @@ which breaks the moment they switch windows. - Showing the agent label inside the Claude Code TUI (status line, terminal title, custom header). That requires upstream Claude Code / codex support. Writing to `claude.json` is best-effort scaffolding for when that lands. -- Per-bottle color affecting anything outside the dashboard agents pane (e.g., - proposal-pane highlights, log prefixes). +- Per-bottle color affecting anything outside the supervisor agents pane. - Validating or constraining label content beyond the 64-byte printable cap. -- Persisting color-pair state across dashboard restarts (color pairs are +- Persisting color-pair state across supervisor restarts (color pairs are initialized fresh each session). - Editing the label or color of an already-running bottle. -- Exposing label/color via `./cli.py list` (out of scope for v1; trivial to - add later since the field will be in metadata). +- Exposing label/color via `./cli.py list` (out of scope for v1). ## Design ### Data flow ``` -operator input +operator input (modal) │ ▼ BottleSpec.label, BottleSpec.color │ - ├─► docker/prepare.py → BottleMetadata.label / .color → metadata.json + ├─► docker/prepare.py → BottleMetadata.label / .color → metadata.json + ├─► smolmachines/prepare.py → BottleMetadata.label / .color → metadata.json │ └─► contrib/claude/agent_provider.py → claude.json {"name": label, "color": color} (omitted when empty) -dashboard refresh +supervisor refresh │ ▼ enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color │ ▼ -_format_agent_row → label (colored) in the row string +agent row → label (colored) in the row string ``` ### BottleSpec changes @@ -158,12 +160,11 @@ class ActiveAgent: `enumerate_active()` copies `label` and `color` out of `BottleMetadata` when constructing each `ActiveAgent`. The smolmachines backend gets the same -additions for symmetry; it reads from its own metadata path. +additions for symmetry. -### Dashboard row rendering +### Agent row rendering -`_format_agent_row` already falls through cleanly on missing fields. The -change is: +The display name logic becomes: ```python display_name = a.label if a.label else a.agent_name @@ -171,10 +172,9 @@ display_name = a.label if a.label else a.agent_name Color rendering uses the existing `_try_init_green()` pattern as a model. A `_color_pair_for(color_name)` helper initialises a fresh curses color pair -for the requested named color and returns its attr (or 0 on failure). Each -unique color in the active agent list gets its own pair index. Color pairs are -allocated lazily and cached in a `dict[str, int]` that lives for the duration -of the dashboard session. +for the requested named color and returns its attr (or 0 on failure). Color +pairs are allocated lazily and cached in a `dict[str, int]` that lives for +the duration of the supervisor session. The 16 ANSI color name → curses constant mapping: @@ -191,44 +191,72 @@ The 16 ANSI color name → curses constant mapping: | `bright-*` | same constant + `curses.A_BOLD` | Terminals that don't support color fall back to plain text (the helper returns -0, which ORed in is a no-op — same pattern as `_try_init_green`). +0, which ORed in is a no-op). -### Label + color prompt — dashboard +### The label+color modal -In `_new_agent_flow`, after `_picker_modal` returns a non-None name and before -`_backend_picker_modal`: +A single curses modal (`name_color_modal` in `bot_bottle/cli/tui.py`) handles +both label and color in two sequential steps within the same window. It is +called identically from the supervisor flow and from `cmd_start`. ```python -label, color = _label_color_modal(stdscr, default_label=picked) +label, color = name_color_modal(default_label=f"{agent_name}-{slug_suffix}") ``` -`_label_color_modal` uses `curses.endwin()` → text-mode prompts → restore -(the same drop-and-resume pattern as the existing editor flow and preflight -Y/N). Two sequential prompts: +**Step 1 — label.** The window renders: ``` -bot-bottle: agent label [implementer]: -bot-bottle: color (red/green/blue/… or Enter to skip): + Name agent + ────────────────────────────────────── + implementer-a3f9 + ────────────────────────────────────── + [any key] edit [Enter] confirm ``` -Invalid color names are silently ignored (treated as empty). The function -returns `(label, color)` — both strings, both possibly `""`. +The pre-filled text is shown in the input field. Any printable keystroke +immediately clears the pre-fill and starts a new name from that character +(first-keystroke-replaces semantics). Subsequent keystrokes append normally. +Backspace edits from the right. Enter confirms — accepting the pre-fill if +the field was never edited, or the typed text otherwise. -### Label + color prompt — CLI +**Step 2 — color.** After confirming the label, the window transitions to: -In `cmd_start`, after argument parsing and before `_launch_bottle`: - -```python -label = _text_prompt_label(args.name) -color = _text_prompt_color() +``` + Name agent + ────────────────────────────────────── + implementer-a3f9 ← confirmed label + ────────────────────────────────────── + Color (optional) + > (none) + red + green + blue + … + ────────────────────────────────────── + [↑↓] move [Enter] select [Esc] skip ``` -`_text_prompt_label(default)` writes `"bot-bottle: agent label [{default}]: "` -to stderr and returns the stripped input (or `default` if blank). -`_text_prompt_color()` writes the color prompt and returns the stripped input -(or `""` if blank or invalid). +The list starts with `(none)` selected. Arrow keys move the cursor; Enter +confirms the highlighted choice; Esc or `q` skips color (equivalent to +selecting `(none)`). Each color name in the list is rendered in its own color +so the operator can preview the palette. -Both use `read_tty_line()` (already in `start.py`) for the read. +The function returns `(label, color)` — both strings, `color` is `""` when +`(none)` is selected or the step is skipped. + +### Slug suffix for the default label + +The default label is `-`, where `slug_suffix` is the +last four characters of the slug (the same short hash shown in the agents +pane). Both the supervisor and `cmd_start` have access to the slug after +`bottle_identity()` is called; the default label is computed there and passed +to `name_color_modal`. + +In `cmd_start` the slug is not yet known at the time the modal appears (it is +minted inside `prepare`). The modal is therefore called with +`default_label=args.name` (the manifest agent key) as a simpler fallback; the +supervisor path, which has the slug available from `_new_agent_flow`, uses the +full `-` form. ### Claude Code config injection @@ -237,13 +265,11 @@ Per PRD 0050, the `claude.json` trust-marker file is written by `bot_bottle/contrib/claude/agent_provider.py`. Add `label: str = ""` and `color: str = ""` keyword parameters to `provision_plan()` on both the `AgentProvider` ABC and `ClaudeAgentProvider`, and to the -`agent_provision_plan()` shim in `agent_provider.py`. The docker and -smolmachines `prepare.py` modules pass `spec.label` / `spec.color` when -calling `agent_provision_plan()`; other providers accept the params and ignore -them. +`agent_provision_plan()` shim in `agent_provider.py`. Both `prepare.py` +modules pass `spec.label` / `spec.color`; other providers accept the params +and ignore them. -In `ClaudeAgentProvider.provision_plan()`, expand the JSON payload -conditionally: +In `ClaudeAgentProvider.provision_plan()`: ```python payload = { @@ -267,27 +293,26 @@ Two PRs, each independently mergeable. - Add `label: str = ""` and `color: str = ""` to `BottleSpec`, `BottleMetadata`, and `ActiveAgent`. -- `docker/prepare.py`: copy `spec.label` / `spec.color` into `BottleMetadata`. -- `docker/enumerate.py`: copy `metadata.label` / `metadata.color` into - `ActiveAgent`. +- `docker/prepare.py` and `smolmachines/prepare.py`: copy `spec.label` / + `spec.color` into `BottleMetadata`; pass them to `agent_provision_plan()`. +- `docker/enumerate.py` and smolmachines equivalent: copy `metadata.label` / + `metadata.color` into `ActiveAgent`. - Add `label: str = ""` and `color: str = ""` keyword params to `AgentProvider.provision_plan()` (ABC), `ClaudeAgentProvider.provision_plan()` (uses them in the `claude.json` write), and the `agent_provision_plan()` shim. - Other providers (`CodexAgentProvider`) accept the params and ignore them. -- `docker/prepare.py` and `smolmachines/prepare.py`: pass `spec.label` / - `spec.color` to `agent_provision_plan()`. -- Smolmachines backend: parallel changes to metadata read/write and - `ActiveAgent` construction. + `CodexAgentProvider` accepts the params and ignores them. - No prompt changes; no UI changes. All existing behavior is identical. -### Chunk 2 — prompts + display +### Chunk 2 — modal + display -- `start.py`: add `_text_prompt_label` and `_text_prompt_color`; call them in - `cmd_start` before `_launch_bottle`; pass `label` / `color` into `BottleSpec`. -- `dashboard.py`: add `_label_color_modal` (drop-and-resume); call it in - `_new_agent_flow`; pass label/color into `BottleSpec`; add - `_color_pair_for` helper; update `_format_agent_row` to use `a.label` with - color rendering. +- `bot_bottle/cli/tui.py`: add `name_color_modal(default_label)` implementing + the two-step curses window described above. +- Supervisor `_new_agent_flow`: call `name_color_modal` after agent selection, + pass `label` / `color` into `BottleSpec`. +- `cmd_start`: call `name_color_modal(default_label=args.name)` before + `_launch_bottle`; pass `label` / `color` into `BottleSpec`. +- Supervisor agent row: add `_color_pair_for` helper; update row rendering to + use `a.label` with color. ## Open questions