# Voice → CLI Selector Inventory

Captures every UI surface where a voice flow picks (or implicitly hardcodes) the
agent / CLI it talks to. Target state: a single shared
`<agent-cli-selector>` mounted at every site, with a uniform vocabulary
mapped to `CoderBackendId` (`claude-code` | `opencode` | `codex`) plus the
in-process `gateway` agent.

Vocabulary today is split:

- **CoderBackend ids** (`src/agents/coder-backend/types.ts`):
  `"claude-code" | "opencode" | "codex"` — used by the spawn registry.
- **UI / PTY `cli` strings** (`terminals.ts`, `app-gateway.ts`, etc.):
  `"claude" | "opencode" | "gemini" | "shell"` — chooses what CLI binary the
  PTY launches.
- **Voice routing setting** (`settings.voice_route`):
  `"gateway_agent" | "claude_code"` — picks gateway agent vs. drive-a-terminal.

These three vocabularies need a single adapter at the boundary. New file
`ui/src/ui/lib/coder-backend-id.ts` does the mapping:

```
UI cli "claude"   ↔ backend "claude-code"
UI cli "opencode" ↔ backend "opencode"
UI cli "codex"    ↔ backend "codex"      (new — no UI vocabulary before)
UI cli "gemini"   ↔ (no CoderBackend yet — shown disabled with hint)
UI cli "shell"    — not an agent; never appears in the voice selector.
```

## Inventory

| # | Surface | File | Hardcode site (line) | Vocab | Current selector | Flows into | Target |
|---|---------|------|----------------------|-------|------------------|------------|--------|
| 1 | Voice chat — main view | `ui/src/ui/views/voice-chat.ts` | filters `cliType === 'claude'` at L5211, L5895, L7054; spawns `cliType: 'claude'` at L6319 | UI `"claude"` | None — Claude implicit | `terminal.create` + `terminal.claude.invoke` + voice-stream | Mount selector in header (group=gateway/cli, persists per-surface) |
| 2 | Voice mini-orb (floating) | `ui/src/ui/components/voice-mini-orb.ts` | No CLI choice — purely visual pulse, click reopens sidebar | n/a | None | Dispatches `infinibot:live-action-mode` only | No selector needed (passive indicator). Inherits whatever surface owns the call. |
| 3 | Voice blob (visualization) | `ui/src/ui/components/voice-blob.ts` | WebGL only, no agent routing | n/a | None | n/a | No selector. |
| 4 | Voice terminal controller | `ui/src/ui/components/voice-terminal-controller.ts` | VAD → STT → `terminal.write`; consumer (voice-commands page today) decides the terminal | implicit | None — relies on caller-passed `terminalId` | `terminal.write` for whichever terminal the consumer mounted | Selector lives in the host view; controller just receives the chosen backend id and passes it to spawnAgent when no terminal is mounted |
| 5 | Live action toast | `ui/src/ui/components/live-action-toast.ts` | Status strings only, no CLI choice | n/a | None | View-name string mapper | No selector (passive overlay). |
| 6 | Voice commands page | `ui/src/ui/views/voice-commands.ts` | "AGENT COMMANDS · open voice for an agent" section binds wake-words to agents | n/a | None for CLI — only agent list | `voicewake.set` triggers | Per-wake-word selector for which CLI handles the utterance (deferred — agent-list bindings already cover the gateway-agent case) |
| 7 | Doc-agent panel voice toggle | `ui/src/ui/components/doc-agent-panel.ts` | Voice button at L781-788; mode set via `doc_agent.set_modalities` (`modalities.voice`) | n/a | Toggle button only | `doc_agent.set_modalities` + `infinibot:doc-agent-voice` event | Add small selector next to the 🎙 toggle so each doc agent can route to a chosen CLI (or the gateway agent which is the default) |
| 8 | Agent activity dashboard | `ui/src/ui/views/agent-activity*.ts` | No voice-specific CLI selection | n/a | None | n/a | No selector needed. |
| 9 | Grid voice controller | `ui/src/ui/controllers/grid-voice.ts` | Per-panel wake-words; transcript routed to `onTranscript` callback per panel | n/a | Per-panel only | Panel-specific dispatch (terminals, infini-chat) | The panel's own selector chooses the CLI — grid-voice stays CLI-agnostic. |
| 10 | App gateway voice dispatch | `ui/src/ui/app-gateway.ts` | `const cli: "claude" | "opencode" | "gemini" | "shell"` at L682, L742 | UI cli union | None — routes by `cliType` | Read from `mapUiCliToBackend(cli)` when forwarding to voice-stream / spawn endpoints |
| 11 | Server voice stream | `src/gateway/server-voice-stream.ts` | Hardcoded `createClaudeCoderTool({ agentId })` at L530 for `voice_route === 'claude_code'` | backend implicit | None | Spawn via `getCoderBackend()` (config) | Honour a per-request `backend` hint; fall back to config |

## CLI vocab reconciliation

Adapter module `ui/src/ui/lib/coder-backend-id.ts` exports:

- `mapUiCliToBackend(cli: "claude" | "opencode" | "codex" | "gemini" | "shell"): CoderBackendId | null`
- `mapBackendToUiCli(id: CoderBackendId): "claude" | "opencode" | "codex"`
- `BACKEND_DISPLAY: Record<CoderBackendId, string>` for selector labels

`"shell"` and `"gemini"` return `null` from `mapUiCliToBackend` — `shell` is
not an agent, `gemini` has no CoderBackend implementation yet (shown disabled
in the selector with an install hint).

## Selector mount plan (Phase 3)

| Site | Mount location | Surface id (localStorage key) |
|------|----------------|--------------------------------|
| voice-chat | header strip next to Connect / Settings buttons | `voice-chat` |
| doc-agent-panel | inside `.panel-head`, left of 🎙 toggle | `doc-agent:<docType>` |
| voice-commands | "AGENT COMMANDS" section — global default | `voice-commands:default` |
| voice-mini-orb | none (passive) | — |
| live-action-toast | none (passive) | — |

## Known blockers acknowledged

- `src/agents/coder-backend/registry.ts:30` — codex branch missing, fix in Phase 4.
- `src/gateway/server-coder-backend.ts:50-74` — `/api/coder/backends` only
  lists claude-code + opencode. Phase 2 extends it to include codex + exposes
  a matching `coder.backends.list` RPC.
- UI build is blocked by missing `grid-panels/{database,projects,tasks}-panel.ts`
  (A3's lane) — out of scope, do not touch.
