diff --git a/docs/prds/prd-new-named-labelled-agents.md b/docs/prds/prd-new-named-labelled-agents.md index b15183b..c75a545 100644 --- a/docs/prds/prd-new-named-labelled-agents.md +++ b/docs/prds/prd-new-named-labelled-agents.md @@ -12,22 +12,22 @@ 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. +rendered in the chosen ANSI color — in `cli list active` output, 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 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: +`cli list active` 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 output shows: ``` - [docker] a3f9 implementer started 14:02:11 [egress,pipelock] - [docker] b81c implementer started 14:03:45 [egress,pipelock] - [docker] d220 implementer started 14:05:01 [egress,pipelock] +docker a3f9 implementer egress,pipelock +docker b81c implementer egress,pipelock +docker d220 implementer egress,pipelock ``` There is no way to tell which bottle is working on which task without attaching @@ -37,34 +37,33 @@ which breaks the moment they switch windows. ## Goals / Success Criteria -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. +1. After the operator selects an agent (picker or CLI argument) and backend, + a curses modal appears before the preflight. The modal pre-fills the label + with `-` (the same pattern currently shown in + `list active`). 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. + keys, or Enter / Esc 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). 5. `ActiveAgent` carries `label` and `color`; `enumerate_active()` reads them from `metadata.json`. -6. The supervisor's agent row uses the label when non-empty (falling back to +6. `cli list active` shows 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. + the label is prefixed with the appropriate ANSI escape code and reset + afterward. 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 +9. `cmd_start` calls `name_color_modal` after backend selection and before + `_launch_bottle`; passes `label` / `color` into `BottleSpec`. +10. 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). @@ -73,12 +72,8 @@ 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 supervisor agents pane. - Validating or constraining label content beyond the 64-byte printable cap. -- 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). ## Design @@ -96,13 +91,13 @@ BottleSpec.label, BottleSpec.color └─► contrib/claude/agent_provider.py → claude.json {"name": label, "color": color} (omitted when empty) -supervisor refresh +cli list active │ ▼ enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color │ ▼ -agent row → label (colored) in the row string +cmd_list → label (with ANSI color) in the row string ``` ### BottleSpec changes @@ -162,42 +157,48 @@ class ActiveAgent: constructing each `ActiveAgent`. The smolmachines backend gets the same additions for symmetry. -### Agent row rendering +### `cli list active` rendering -The display name logic becomes: +The current row format is tab-separated: +`{backend}\t{slug}\t{agent_name}\t{services}` +With labels it becomes: ```python 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). Color -pairs are allocated lazily and cached in a `dict[str, int]` that lives for -the duration of the supervisor session. +Color is rendered via ANSI escape codes. A small `_ansi_color(color_name)` +helper returns the appropriate escape prefix for the 16 named colors, or `""` +when the name is unrecognised or the terminal doesn't support color +(`NO_COLOR` env var or `not sys.stdout.isatty()`). -The 16 ANSI color name → curses constant mapping: +The 16 ANSI color name → escape mapping: -| Name | curses constant | -|------|----------------| -| `black` | `curses.COLOR_BLACK` | -| `red` | `curses.COLOR_RED` | -| `green` | `curses.COLOR_GREEN` | -| `yellow` | `curses.COLOR_YELLOW` | -| `blue` | `curses.COLOR_BLUE` | -| `magenta` | `curses.COLOR_MAGENTA` | -| `cyan` | `curses.COLOR_CYAN` | -| `white` | `curses.COLOR_WHITE` | -| `bright-*` | same constant + `curses.A_BOLD` | +| Name | ANSI code | +|------|-----------| +| `black` | `\033[30m` | +| `red` | `\033[31m` | +| `green` | `\033[32m` | +| `yellow` | `\033[33m` | +| `blue` | `\033[34m` | +| `magenta` | `\033[35m` | +| `cyan` | `\033[36m` | +| `white` | `\033[37m` | +| `bright-black` | `\033[90m` | +| `bright-red` | `\033[91m` | +| `bright-green` | `\033[92m` | +| `bright-yellow` | `\033[93m` | +| `bright-blue` | `\033[94m` | +| `bright-magenta` | `\033[95m` | +| `bright-cyan` | `\033[96m` | +| `bright-white` | `\033[97m` | -Terminals that don't support color fall back to plain text (the helper returns -0, which ORed in is a no-op). +Reset is `\033[0m`. Applied around the label substring only. ### The label+color 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`. +both label and color in two sequential steps within the same window. ```python label, color = name_color_modal(default_label=f"{agent_name}-{slug_suffix}") @@ -237,9 +238,9 @@ the field was never edited, or the typed text otherwise. ``` 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. +confirms the highlighted choice; Esc or `q` skips color. Each color name in +the list is rendered in its own curses color so the operator can preview the +palette. The function returns `(label, color)` — both strings, `color` is `""` when `(none)` is selected or the step is skipped. @@ -247,16 +248,15 @@ The function returns `(label, color)` — both strings, `color` is `""` when ### 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`. +last four characters of the slug (the same short hash shown in `list active`). -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. +In `cmd_start` the slug is minted inside `prepare`, after the modal appears. +The modal is therefore called with the manifest agent key as a fallback +(`default_label=agent_name`). Once `prepare` returns the plan (which contains +the slug), the `BottleSpec` is not reconstructed — the label entered by the +operator is already in the spec. The full `-` form is +only available for display in subsequent `list active` calls once the bottle +is running. ### Claude Code config injection @@ -266,8 +266,8 @@ Per PRD 0050, the `claude.json` trust-marker file is written by `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`. Both `prepare.py` -modules pass `spec.label` / `spec.color`; other providers accept the params -and ignore them. +modules pass `spec.label` / `spec.color`; `CodexAgentProvider` accepts the +params and ignores them. In `ClaudeAgentProvider.provision_plan()`: @@ -301,18 +301,17 @@ Two PRs, each independently mergeable. `AgentProvider.provision_plan()` (ABC), `ClaudeAgentProvider.provision_plan()` (uses them in the `claude.json` write), and the `agent_provision_plan()` shim. `CodexAgentProvider` accepts the params and ignores them. +- `cmd_list`: update `list active` row to use `label` when non-empty, with + ANSI color escape codes. - No prompt changes; no UI changes. All existing behavior is identical. -### Chunk 2 — modal + display +### Chunk 2 — modal - `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. +- `cmd_start`: call `name_color_modal(default_label=agent_name)` after backend + selection and before `_launch_bottle`; pass `label` / `color` into + `BottleSpec`. ## Open questions