Files
bot-bottle/docs/prds/prd-new-named-labelled-agents.md
T
didericis 39e0976ace
lint / lint (push) Successful in 1m44s
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 <agent_name>-<slug_suffix>; 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 <noreply@anthropic.com>
2026-06-07 12:01:11 -04:00

12 KiB

PRD prd-new: Named / Labelled Agents

  • Status: Draft
  • Author: didericis
  • Created: 2026-06-03
  • Issue: #171

Summary

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 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:

  [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]

There is no way to tell which bottle is working on which task without attaching to each one in turn. The slug is opaque; the manifest key is shared. Operators working a multi-bottle session resort to keeping a mental map of slug→task, 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 <agent_name>-<slug_suffix> (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).
  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 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).

Non-goals

  • 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

Data flow

operator input (modal)
     │
     ▼
BottleSpec.label, BottleSpec.color
     │
     ├─► 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)

supervisor refresh
     │
     ▼
enumerate_active() → read_metadata(slug) → ActiveAgent.label / .color
     │
     ▼
agent row → label (colored) in the row string

BottleSpec changes

@dataclass(frozen=True)
class BottleSpec:
    manifest: Manifest
    agent_name: str
    copy_cwd: bool
    user_cwd: str
    identity: str = ""
    label: str = ""   # operator-chosen display name; defaults to agent_name at render time
    color: str = ""   # one of the 16 ANSI color names, or "" for terminal default

label and color default to "" so all existing callers remain valid with no changes.

BottleMetadata changes

Add two new fields with backward-compatible defaults:

@dataclass
class BottleMetadata:
    identity: str
    agent_name: str
    cwd: str
    copy_cwd: bool
    started_at: str
    compose_project: str
    backend: str
    label: str = ""
    color: str = ""

metadata.json written by older bot-bottle versions won't have these keys; read_metadata already uses dict.get with defaults, so existing slugs load cleanly with label="", color="".

ActiveAgent changes

@dataclass(frozen=True)
class ActiveAgent:
    backend_name: str
    slug: str
    agent_name: str
    started_at: str
    services: tuple[str, ...]
    label: str = ""
    color: str = ""

enumerate_active() copies label and color out of BottleMetadata when constructing each ActiveAgent. The smolmachines backend gets the same additions for symmetry.

Agent row rendering

The display name logic becomes:

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.

The 16 ANSI color name → curses constant 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

Terminals that don't support color fall back to plain text (the helper returns 0, which ORed in is a no-op).

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.

label, color = name_color_modal(default_label=f"{agent_name}-{slug_suffix}")

Step 1 — label. The window renders:

  Name agent
  ──────────────────────────────────────
  implementer-a3f9
  ──────────────────────────────────────
  [any key] edit  [Enter] confirm

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.

Step 2 — color. After confirming the label, the window transitions to:

  Name agent
  ──────────────────────────────────────
  implementer-a3f9  ← confirmed label
  ──────────────────────────────────────
  Color (optional)
  > (none)
    red
    green
    blue
    …
  ──────────────────────────────────────
  [↑↓] move  [Enter] select  [Esc] skip

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.

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 <agent_name>-<slug_suffix>, 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 <agent_name>-<slug_suffix> form.

Claude Code config injection

Per PRD 0050, the claude.json trust-marker file is written by ClaudeAgentProvider.provision_plan() in 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. Both prepare.py modules pass spec.label / spec.color; other providers accept the params and ignore them.

In ClaudeAgentProvider.provision_plan():

payload = {
    "hasCompletedOnboarding": True,
    "theme": "dark",
    "bypassPermissionsModeAccepted": True,
    "projects": claude_projects,
}
if label:
    payload["name"] = label
if color:
    payload["color"] = color
claude_config.write_text(json.dumps(payload, indent=2) + "\n")

Implementation chunks

Two PRs, each independently mergeable.

Chunk 1 — schema + storage

  • Add label: str = "" and color: str = "" to BottleSpec, BottleMetadata, and 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. CodexAgentProvider accepts the params and ignores them.
  • No prompt changes; no UI changes. All existing behavior is identical.

Chunk 2 — modal + display

  • 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

None.