95 lines
7.8 KiB
Markdown
95 lines
7.8 KiB
Markdown
# ADR-017: Charm TUI client — extracting `pi-tui` out of sf core
|
||
|
||
**Date**: 2026-04-29
|
||
**Status**: proposed (deferred — capture for staged execution)
|
||
|
||
## Context
|
||
|
||
sf today bundles its TUI directly in core: `pi-tui` (~10.5k LOC of TypeScript) is loaded whenever the user interacts with sf. The TUI lives at the same architectural layer as the agent loop, the auto-loop, and the planner. This couples *what sf does* to *how it presents*.
|
||
|
||
Three forces argue for extracting the TUI:
|
||
|
||
1. **sf is becoming truly headless-first** — `packages/daemon` and `packages/rpc-client` already exist. CLI invocations talk to the daemon. SF uses MCP as a client integration surface for external tools, not as an SF workflow server. The user-facing TUI is *one client*; it shouldn't be *baked into the engine*.
|
||
2. **The Charm TUI stack is dramatically more capable than what `pi-tui` builds today.** `bubbletea` + `bubbles` + `lipgloss` + `glamour` + `huh` + `harmonica` + `x/mosaic` (image rendering) + `x/vcr` (recording) + `pony` + `ultraviolet` (declarative markup) compose to far better UX than reproducing in TS would.
|
||
3. **Removing `pi-tui` from sf core deletes ~10k LOC of TS** — leaner core, fewer TUI-coupled assumptions in `pi-coding-agent`, cleaner test surface.
|
||
|
||
This ADR plans the extraction.
|
||
|
||
## Decision
|
||
|
||
- **Build a new `sf-tui` client in Go** using the Charm stack. Talks to the sf daemon over the existing RPC (per `packages/rpc-client`).
|
||
- **View layer: `pony` (declarative TUI markup) + `ultraviolet` (its base).** Adopted now, not deferred. Other view primitives where pony lacks coverage: `bubbles` components, `lipgloss` styling, `glamour` markdown, `huh` forms, `harmonica` animations, `x/mosaic` for inline images.
|
||
- **Two-stage replacement of `pi-tui`:**
|
||
- **Stage 1:** new `sf-tui` ships parallel to `pi-tui`. Users opt-in via `sf --tui=charm`. `pi-tui` remains the default. Both clients connect to the same daemon — they're peer clients, not replacements yet.
|
||
- **Stage 2:** when `sf-tui` reaches parity (every screen `pi-tui` has, plus the new ones the Charm stack enables), flip the default. Deprecate `pi-tui` with a warning. After two minor releases, **delete `pi-tui` entirely** — ~10k LOC of TS dropped from sf core.
|
||
- **No migration of in-flight `pi-tui` work.** Anything in `pi-tui` that hasn't shipped doesn't get backported to `sf-tui`. The new client is a clean slate.
|
||
- **Architecture: clean separation between view rendering and state/data layer.** State models live in their own package; view components consume them. If `pony` proves unworkable, the swap to plain `bubbletea` is a view-layer-only refactor.
|
||
|
||
## Alternatives Considered
|
||
|
||
- **Replace `pi-tui` in-place with a TS port of Bubble Tea.** No mature TS port exists. Even if one were started, Charm's TUI ecosystem (Bubbles, Lipgloss, Glamour, Huh, etc.) wouldn't follow.
|
||
- *Rejected:* equivalent to "rebuild the Charm stack in TS." Years of work for no advantage.
|
||
- **Embed Bubble Tea inside `pi-coding-agent` via cgo / WebAssembly.**
|
||
- *Rejected:* fragile FFI; defeats the architectural goal of separating engine from UI.
|
||
- **Keep `pi-tui` indefinitely; only build Charm TUI as an alternative for SSH access.**
|
||
- *Rejected:* leaves ~10k LOC of TS in sf core *forever* as a maintenance burden. The whole point is to delete it.
|
||
- **Don't build a new TUI; expose the daemon over an external API and rely on third-party clients (Claude Code, Cursor) to render.**
|
||
- *Rejected:* sf's user-facing surface is the TUI when working interactively. Outsourcing it removes a major UX touchpoint we own.
|
||
|
||
## Consequences
|
||
|
||
**Positive**
|
||
|
||
- **sf core gets ~10k LOC leaner** after Stage 2.
|
||
- **Charm stack quality** comes for free — animations (`harmonica`), inline images (`x/mosaic`), markdown (`glamour`), forms (`huh`), recording (`x/vcr`).
|
||
- **Headless / API-first architecture** is cleanly visible: daemon + RPC + clients, with MCP client integration for external tools. No TUI coupled to engine.
|
||
- **Remote TUI for free** — once the client is Wish-served (could be a v3.x extension), `tailscale ssh aidev sf` opens a full TUI session over SSH. Today's `pi-tui` is local-process only.
|
||
- **Recordings of TUI sessions** — flight recorder (ADR-015) integrates with the Charm TUI naturally; `pi-tui` would need separate work to support this.
|
||
|
||
**Negative**
|
||
|
||
- **Two-language UI work during Stage 1** — bug fixes touching both `pi-tui` (TS) and `sf-tui` (Go). Bounded duration; one client retires at Stage 2.
|
||
- **Pony is pre-1.0** — API churn during the build. Acceptable per the "view layer swappable" architecture.
|
||
- **User-facing transition** — users have to relearn keybindings or layouts if `sf-tui` differs from `pi-tui`. Mitigated by explicit parity gate: `sf-tui` must match `pi-tui`'s primary views before Stage 2 flip.
|
||
- **Daemon RPC contract becomes load-bearing** — what was previously an in-process call (TS → TS) is now a cross-process call (Go → TS via RPC). Requires the RPC contract to be stable and complete; missing methods become blockers. Acceptable; this is the right architectural pressure.
|
||
|
||
**Risks and mitigations**
|
||
|
||
- *Risk:* parity gate is moved unilaterally (Stage 2 flips default before parity is real).
|
||
- *Mitigation:* parity defined explicitly as a checklist of `pi-tui` screens with their `sf-tui` equivalents and end-to-end tests passing. CI gate.
|
||
- *Risk:* `pony` proves unstable; we hit the swap-to-`bubbletea` fallback halfway.
|
||
- *Mitigation:* view layer is architected to be swappable (pony components implement an interface; bubbletea components implement the same interface). Swap is a refactor, not a rewrite.
|
||
- *Risk:* Daemon RPC has gaps that `pi-tui` papers over via in-process state access.
|
||
- *Mitigation:* audit `pi-tui`'s direct daemon-state access at the start of Stage 1; promote any in-process patterns to RPC methods.
|
||
- *Risk:* User keybindings / muscle memory breaks.
|
||
- *Mitigation:* `sf-tui` mirrors `pi-tui`'s keybindings 1:1 for the parity surface; new keybindings only for new features.
|
||
|
||
## Out of Scope
|
||
|
||
- **Web-based UI.** Could be a separate v4 project.
|
||
- **Multi-user TUI sessions** (two operators watching the same auto-loop).
|
||
- **Theme customisation.** v1 ships one theme; user theming is later.
|
||
- **Internationalisation.** v1 is English only; same posture as today.
|
||
|
||
## Sequencing
|
||
|
||
| Stage | Action | Cost | Result |
|
||
|---|---|---|---|
|
||
| Pre-stage | Audit `pi-tui` screens; produce a parity checklist. | 1 week | List of screens + features `sf-tui` must cover. |
|
||
| Stage 1 | Build `sf-tui` parallel to `pi-tui`. View on pony+ultraviolet+bubbles, state separate. Daemon RPC fills any gaps. Ships as opt-in via `sf --tui=charm`. | ~6–10 weeks | Two TUIs coexist. Users pick. |
|
||
| Stage 1.5 | Parity verification — every checklist item works in `sf-tui`; CI gate. | 2 weeks | `sf-tui` ready to flip default. |
|
||
| Stage 2 | Flip default to `sf-tui`. Deprecate `pi-tui` with warning on use. | 1 week + soak | `sf-tui` is canonical; `pi-tui` is legacy. |
|
||
| Stage 3 | Delete `pi-tui` after two minor releases. | 1 week cleanup | sf core sheds ~10k LOC of TS. |
|
||
|
||
Total: ~12–16 weeks across stages.
|
||
|
||
## References
|
||
|
||
- `packages/daemon`, `packages/rpc-client` — already exist; this ADR makes them load-bearing for clients.
|
||
- `packages/pi-tui` — the existing TUI being deprecated.
|
||
- `ADR-013` — Network: future SSH-served TUI via `wish` rides the same substrate.
|
||
- `ADR-015` — Flight recorder: `sf-tui` records its sessions naturally.
|
||
- `ADR-016` — Charm AI stack adoption (this is one of its concrete arms).
|
||
- `charmbracelet/bubbletea`, `charmbracelet/bubbles`, `charmbracelet/lipgloss`, `charmbracelet/glamour`, `charmbracelet/huh`, `charmbracelet/harmonica`.
|
||
- `charmbracelet/x/mosaic`, `charmbracelet/x/vcr`, `charmbracelet/x/editor`, `charmbracelet/x/input`.
|
||
- `charmbracelet/pony` + `charmbracelet/ultraviolet` — adopted as the view-layer foundation.
|