Files
bot-bottle/docs/prds/0051-launch-selector.md
T
didericis cd9f023f3d
prd-number-check / require-numbered-prds (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (push) Successful in 21s
test / image-input-builds (push) Successful in 44s
test / image-input-builds (pull_request) Successful in 1m15s
test / unit (push) Successful in 57s
test / coverage (pull_request) Successful in 20s
lint / lint (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m12s
test / integration-docker (push) Successful in 1m0s
docs: name bot-bottle, not ./cli.py, in every instruction
`doctor` told users to run `./cli.py backend setup`. cli.py is a four-line
wrapper at the repo root that calls bot_bottle.cli:main, and it does not ship —
[tool.setuptools.packages.find] includes only bot_bottle*, so anyone who
installed rather than cloned has no such file. Confirmed against a real
install: the user's tree contains exactly one executable, `bot-bottle`, and no
cli.py anywhere, while doctor recommended `./cli.py` three times. Invisible in
development, where ./cli.py works fine from a checkout, which is why only a
clean-install test surfaced it.

The two entry points are the same code, so the fix is to name the one that
always exists. 141 replacements across 45 files: the runtime messages that
caused this, plus README, docs, PRDs, research notes and test prose, so nothing
teaches the invocation a user cannot run.

scripts/demo.sh is deliberately untouched — it *executes* ./cli.py from a
checkout, where that is the correct and available path.

Verified end to end: a sandbox install now reports
"Run: bot-bottle backend setup --backend=firecracker", and bot-bottle is on
that user's PATH. Unit suite unchanged against the pre-existing baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 10:20:17 -04:00

5.6 KiB

PRD 0051: Launch selector

  • Status: Active
  • Author: claude
  • Created: 2026-06-04
  • Issue: #185

Summary

When bot-bottle start is run without an agent name, or without a backend explicitly specified, the user currently gets an argparse error (missing positional) or falls through to the docker default silently. This PRD adds a terminal UI that appears in those gaps: a filter-select screen built with curses that lets the operator pick the agent and/or backend interactively rather than memorising names or consulting bot-bottle list.

Problem

With the dashboard removed (PRD 0049), starting an agent from memory is the only path. The operator must know the exact agent name and type it as a positional argument. For infrequent users or large manifests this is friction. A picker that appears automatically when the name is absent closes the gap with minimal ceremony.

The same logic applies to backends: the operator rarely wants to specify --backend explicitly, but when they do they need to know the set of registered names. A picker on an empty --backend makes the choice visible.

Goals / Success Criteria

  1. bot-bottle start (no arguments) shows an interactive agent selector; the selected name is used exactly as if it had been passed on the command line.
  2. bot-bottle start <name> (no --backend, no BOT_BOTTLE_BACKEND) shows an interactive backend selector; the selected backend is used exactly as if --backend=<selected> had been passed.
  3. bot-bottle start <name> --backend=<b> (both explicit) shows neither screen — no behavioural change from today.
  4. bot-bottle start (no arguments, no env backend) shows the agent selector first, then the backend selector.
  5. The filter-select widget is a standalone utility (bot_bottle/cli/tui.py) shared by both selectors.
  6. Pressing Ctrl-C or q in either selector exits cleanly (exit 0).
  7. The widget supports incremental filtering: typing narrows the list; Backspace removes the last character; //j/k move the cursor; Enter confirms; Esc/q cancels.
  8. Unit tests cover: filtering logic, cursor movement, confirm, cancel, and the cmd_start dispatch (agent-absent, backend-absent, both-explicit, both-absent).

Non-goals

  • The TUI is not a general-purpose picker exposed as a public API; it is an internal CLI utility.
  • No mouse support.
  • No pagination beyond what fits in the terminal window (scroll via cursor movement is sufficient for typical agent counts).
  • No multi-select; exactly one item is chosen per invocation.
  • No changes to bot-bottle resume, bot-bottle list, or any other subcommand.

Design

bot_bottle/cli/tui.pyfilter_select

def filter_select(
    items: list[str],
    *,
    title: str = "",
    tty_path: str = "/dev/tty",
) -> str | None:
    """Render a filter-select picker over the items list.

    Returns the selected item string, or None if the user cancelled
    (Esc / q / Ctrl-C / Ctrl-D).

    Opens /dev/tty directly so the picker works even when stdout/stdin
    are redirected — same pattern as `read_tty_line`.
    """

The widget renders to the tty file descriptor opened via curses.initscr (or curses.newterm on the tty fd so stdout remains clean for callers that pipe bot-bottle).

Layout (full-width, minimal):

  Select agent                   (title, top line)
  Filter: <query>_               (filter line)
  ─────────────────────────────
  > researcher
    implementer
    codex-researcher
    ...
  ─────────────────────────────
  [↑↓/jk] move  [Enter] select  [Esc/q] cancel
  • Lines below the filter are the filtered items; the cursor (>) marks the selection.
  • The list re-renders on every keypress.
  • Terminal resize is not handled (SIGWINCH); if the window is too small the picker exits with None.

Changes to cmd_start

name changes from a required positional to an optional one (nargs="?"). The post-parse block checks:

agent_name = args.name
if agent_name is None:
    manifest = Manifest.resolve(USER_CWD)
    agent_name = tui.filter_select(
        sorted(manifest.agents.keys()),
        title="Select agent",
    )
    if agent_name is None:
        return 0  # user cancelled

backend_name = args.backend
if backend_name is None and "BOT_BOTTLE_BACKEND" not in os.environ:
    backend_name = tui.filter_select(
        list(known_backend_names()),
        title="Select backend",
    )
    if backend_name is None:
        return 0  # user cancelled

The manifest object is resolved before the backend selection so the agent picker can populate itself from the real manifest. The same manifest is passed to BottleSpec; it is not resolved a second time.

/dev/tty isolation

filter_select opens /dev/tty and feeds it as the input file to curses.wrapper-equivalent code (using curses.newterm to avoid clobbering the caller's stdout/stderr). This keeps the picker composable — callers can pipe bot-bottle output without the curses draw sequences contaminating the pipe.

Implementation chunks

  1. tui.py + tests. Add bot_bottle/cli/tui.py with filter_select and unit tests in tests/unit/test_cli_tui.py.
  2. Wire into cmd_start + tests. Make name optional, add the two-gate dispatch, extend tests/unit/test_cli_start_selector.py.
  3. Activate PRD 0051. Flip Status Draft → Active in the same commit that lands the implementation.

Open questions

None. Scope is fully determined by the issue description.