Files
bot-bottle/docs/prds/0051-launch-selector.md
T
didericis-claude 605a70408e
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 41s
test / unit (push) Successful in 35s
test / integration (push) Successful in 42s
feat(cli): add launch selector TUI for start command (PRD 0051)
- Add bot_bottle/cli/tui.py: curses filter-select picker that opens
  /dev/tty directly so it works with redirected stdout/stdin
- Make `name` positional optional (nargs="?") in cmd_start; show agent
  picker when absent
- Show backend picker when no --backend flag and BOT_BOTTLE_BACKEND is
  unset; skip when either is explicit or the env var is present
- Add tests/unit/test_cli_tui.py covering _filter_items logic and
  short-circuit paths (empty list, unavailable tty)
- Add tests/unit/test_cli_start_selector.py covering all four dispatch
  combinations (both explicit, agent-absent, backend-absent, both-absent)
  and cancel semantics
- Activate PRD 0051
2026-06-04 01:54:53 +00:00

5.6 KiB

PRD 0051: Launch selector

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

Summary

When ./cli.py 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 ./cli.py 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. ./cli.py 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. ./cli.py 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. ./cli.py start <name> --backend=<b> (both explicit) shows neither screen — no behavioural change from today.
  4. ./cli.py 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 ./cli.py resume, ./cli.py 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 ./cli.py).

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 ./cli.py 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.