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

```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.
