# 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 ` (no `--backend`, no `BOT_BOTTLE_BACKEND`) shows an interactive backend selector; the selected backend is used exactly as if `--backend=` had been passed. 3. `./cli.py start --backend=` (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.py` — `filter_select` ```python 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: _ (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: ```python 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.