605a70408e
- 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
158 lines
5.6 KiB
Markdown
158 lines
5.6 KiB
Markdown
# 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.
|