From 832808ff9a856844576df4645f9879e78392967c Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 4 Jun 2026 01:52:29 +0000 Subject: [PATCH] =?UTF-8?q?docs(prd):=20draft=20PRD=200051=20=E2=80=94=20l?= =?UTF-8?q?aunch=20selector=20TUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/prds/0051-launch-selector.md | 157 ++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/prds/0051-launch-selector.md diff --git a/docs/prds/0051-launch-selector.md b/docs/prds/0051-launch-selector.md new file mode 100644 index 0000000..4aedd26 --- /dev/null +++ b/docs/prds/0051-launch-selector.md @@ -0,0 +1,157 @@ +# PRD 0051: Launch selector + +- **Status:** Draft +- **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.