- 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>
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
- 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. - 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.
- 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.
labelandcolorare stored inBottleMetadataand written to the bottle'smetadata.json. Both fields default to""(empty / unset).ActiveAgentcarrieslabelandcolor;enumerate_active()reads them frommetadata.json.- 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. BottleSpeccarrieslabelandcolor; both backends'preparesteps copy them intoBottleMetadata.ClaudeAgentProvider.provision_plan()writeslabel→"name"andcolor→"color"into the generatedclaude.json. Fields are omitted when empty.- The supervisor's
_new_agent_flowincludes the modal between agent selection and the backend picker. cmd_start(CLI) shows the same modal (via the sharedtuimodule) before_launch_bottle; passeslabel/colorintoBottleSpec.- 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.jsonis 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 = ""andcolor: str = ""toBottleSpec,BottleMetadata, andActiveAgent. docker/prepare.pyandsmolmachines/prepare.py: copyspec.label/spec.colorintoBottleMetadata; pass them toagent_provision_plan().docker/enumerate.pyand smolmachines equivalent: copymetadata.label/metadata.colorintoActiveAgent.- Add
label: str = ""andcolor: str = ""keyword params toAgentProvider.provision_plan()(ABC),ClaudeAgentProvider.provision_plan()(uses them in theclaude.jsonwrite), and theagent_provision_plan()shim.CodexAgentProvideraccepts 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: addname_color_modal(default_label)implementing the two-step curses window described above.- Supervisor
_new_agent_flow: callname_color_modalafter agent selection, passlabel/colorintoBottleSpec. cmd_start: callname_color_modal(default_label=args.name)before_launch_bottle; passlabel/colorintoBottleSpec.- Supervisor agent row: add
_color_pair_forhelper; update row rendering to usea.labelwith color.
Open questions
None.