7.8 KiB
7.8 KiB
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:
- sf is becoming truly headless-first —
packages/daemonandpackages/rpc-clientalready 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. - The Charm TUI stack is dramatically more capable than what
pi-tuibuilds 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. - Removing
pi-tuifrom sf core deletes ~10k LOC of TS — leaner core, fewer TUI-coupled assumptions inpi-coding-agent, cleaner test surface.
This ADR plans the extraction.
Decision
- Build a new
sf-tuiclient in Go using the Charm stack. Talks to the sf daemon over the existing RPC (perpackages/rpc-client). - View layer:
pony(declarative TUI markup) +ultraviolet(its base). Adopted now, not deferred. Other view primitives where pony lacks coverage:bubblescomponents,lipglossstyling,glamourmarkdown,huhforms,harmonicaanimations,x/mosaicfor inline images. - Two-stage replacement of
pi-tui:- Stage 1: new
sf-tuiships parallel topi-tui. Users opt-in viasf --tui=charm.pi-tuiremains the default. Both clients connect to the same daemon — they're peer clients, not replacements yet. - Stage 2: when
sf-tuireaches parity (every screenpi-tuihas, plus the new ones the Charm stack enables), flip the default. Deprecatepi-tuiwith a warning. After two minor releases, deletepi-tuientirely — ~10k LOC of TS dropped from sf core.
- Stage 1: new
- No migration of in-flight
pi-tuiwork. Anything inpi-tuithat hasn't shipped doesn't get backported tosf-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
ponyproves unworkable, the swap to plainbubbleteais a view-layer-only refactor.
Alternatives Considered
- Replace
pi-tuiin-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-agentvia cgo / WebAssembly.- Rejected: fragile FFI; defeats the architectural goal of separating engine from UI.
- Keep
pi-tuiindefinitely; 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 sfopens a full TUI session over SSH. Today'spi-tuiis local-process only. - Recordings of TUI sessions — flight recorder (ADR-015) integrates with the Charm TUI naturally;
pi-tuiwould need separate work to support this.
Negative
- Two-language UI work during Stage 1 — bug fixes touching both
pi-tui(TS) andsf-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-tuidiffers frompi-tui. Mitigated by explicit parity gate:sf-tuimust matchpi-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-tuiscreens with theirsf-tuiequivalents and end-to-end tests passing. CI gate.
- Mitigation: parity defined explicitly as a checklist of
- Risk:
ponyproves unstable; we hit the swap-to-bubbleteafallback 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-tuipapers 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.
- Mitigation: audit
- Risk: User keybindings / muscle memory breaks.
- Mitigation:
sf-tuimirrorspi-tui's keybindings 1:1 for the parity surface; new keybindings only for new features.
- Mitigation:
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 viawishrides the same substrate.ADR-015— Flight recorder:sf-tuirecords 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.