Building a TUI for an agentic loop is hard. Permission prompts, streaming partials, tool gating, diff apply/reject, MCP catalogs, SSE reconnects, per-session state — every coder (Claude Code, OpenCode, Crush, Goose, …) re-solves these in a slightly different way and locks you into their UI.
GACT inverts that: define the wire contract once, build one good TUI,
then write thin adapters for each backend. If your agent speaks
GACT v0.2 — REST + SSE — the TUI works. The
canonical reference implementation is iowarp/clio-agent,
a scientific-data agent that supports 28 of 30 v0.2 capabilities
end-to-end (only LSP + voice intentionally out). See
SCREENSHOTS.md for live captures of every advertised
feature working through the TUI. Extendable (drop in a new adapter), modifiable
(lipgloss themes + Bubbletea components all the way down), scriptable
(~70 CLI subcommands alongside the interactive TUI).
Every agentic coder ships its own UI. They diverge on small things
(themes, keybindings, paste handling) and lock you into one provider.
GACT inverts that: define the wire contract once, build one good UI,
then write thin adapters for each backend. Adapters live in
adapters/; the contract and conformance suite live in
contract/.
Two ways to run — pick the one that matches your stack:
Against your real agent (Claude Code):
gact agent deploy claudecode myclaude # spawns the adapter detached
gact connect myclaude # interactive TUI
# Ctrl+Z detaches the TUI; the adapter keeps running.
gact resume # comes back where you were
gact agent stop myclaude # when you're done for the dayAgainst the reference emulator (no API keys, no network):
emulator-server --port 7777 --timing realistic &
GACT_BACKEND=http://localhost:7777 gactScripting without the TUI:
gact dashboard --sort newest # live sessions table
gact ask <sid> "summarise the diff" # one-shot Q&A
gact log <sid> --role assistant --grep "error" # filter conversation
gact agent list --format json # registered agents
gact session create --title "scratch" # CRUD alias layerInside the TUI: type a message → watch the thinking stream, tool call
fire, tool result render, reply stream back char-by-char. Try delete the temp dir for the permission flow, propose an edit to main.go for
a file_diff you can apply or reject, or read main.go three times
to see the emulator's per-session variant cycling.
Requires Go 1.25+ and a 256-color (or true-color) terminal.
git clone https://github.com/JaimeCernuda/gact-tui
cd gact-tui
make build && make install # → ~/.local/bin/{gact,emulator-server}Adapters translate GACT v0.1 ↔ a vendor-specific protocol. Each one
ships as a sidecar binary you run between the TUI and the upstream.
Each passes the contract/conformance suite
so the TUI behaves identically across all of them.
| Backend | Adapter | Status |
|---|---|---|
| Claude Code (Go, recommended) | adapters/claudecode/ |
Native Go, drives claude --output-format stream-json directly — single binary, no Python runtime. 16/16 conformance sections ✓ |
| Claude Code (Python) | adapters/claude-agent-sdk-server/ |
Python sidecar on claude-agent-sdk — retained for reference; the Go adapter above is feature-equivalent |
| OpenCode | adapters/opencode/ |
Go proxy of the OpenCode HTTP API |
| Crush | adapters/crush/ |
Go proxy of the Crush HTTP API |
| Goose | adapters/goose/ |
Go proxy of the goosed HTTP API — sessions/messages read+write + SSE; 8/8 conformance sections ✓ |
| CLIO Agent | (in-process, Python) — clio-agent-gact console script ships with clio-agent's tui-integration branch |
v0.2 native: agent_routing (tier-1 → tier-2 experts), memory stats, integration_health (/doctor), tool_telemetry events, cost tracking, forks, search, permissions, two-phase edits, nanoagent spawns. gact agent deploy clio my-clio deploys it. See Quick start with CLIO + Claude Max below. |
Three processes + an env block — Meridian translates your Claude Max OAuth session into the OpenAI-compatible endpoint CLIO's DSPy runtime wants.
# 1. Install Meridian and authenticate (once). Reuses Claude Code's
# OAuth if you already run it locally.
npm install -g @rynfar/meridian
CLAUDE_CONFIG_DIR=$HOME/.claude meridian &
# (or: meridian profile add personal for a fresh browser login)
# 2. Install CLIO's tui-integration branch (once).
git clone -b tui-integration https://github.com/iowarp/clio-agent
cd clio-agent && uv pip install -e '.[api]'
# 3. Point CLIO at Meridian and deploy it as a GACT backend. Haiku
# keeps cost down; Sonnet/Opus are available via Meridian's
# /v1/models.
export CLIO_LM_PROVIDER=openai
export CLIO_LM_API_BASE=http://127.0.0.1:3456/v1
export CLIO_LM_MODEL=claude-haiku-4-5-20251001
export CLIO_LM_API_KEY=x
gact agent deploy clio my-clio
gact connect my-clioThe TUI now renders a real Claude turn through CLIO's tier-1 router → tier-2 expert → answer + routing badge + ARC cache chip + live token/cost rollup in the footer. Tools (HDF5, Parquet MCP servers) need additional config — see CLIO's Meridian doc for the full recipe.
- Read the contract. Every endpoint your adapter has to implement
(or explicitly opt out of via
capabilities.<flag> = false) is incontract/SPEC.md. - Fork an existing adapter.
adapters/crush/oradapters/opencode/are ~400 LOC each and follow the same shape:server.go(HTTP router),transport.go(upstream HTTP client),translate.go(schema ↔ GACT). - Run conformance. Add a
conformance_test.gothat boots your adapter against a mock upstream and callsconformance.Run(t, srv.URL, opts). The suite (contract/conformance/) locks 16+ sections: session CRUD, message + SSE envelope shapes (SPEC §7.2), MCP per-server drill-downs, diff apply/reject, file_diff Parts, agents, tools, metrics, workspaces, permissions control protocol. - Open a PR. If conformance passes, the TUI just works.
The TUI itself is feature-gated on GET /v1/capabilities: adapters
that don't implement e.g. capabilities.diffs automatically hide the
diff workflow in the UI — no per-adapter TUI code. Same for tools,
agents, MCP, permissions, voice.
Highlights — full keybind / CLI / capability reference in
docs/FEATURES.md:
TUI
- 7 themes + custom JSON palettes + Gruvbox-warm / Tokyo Night / Dracula
- Settings modal: model + agent picker, theme cycler, TUI prefs (collapse threshold, cost warn/danger tokens, paste compress, intro splash toggle)
@-file picker (fuzzy basename),/-slash palette,Ctrl+Gcompose modal for long prompts- Bracketed-paste compression →
[pasted content: N lines]placeholder,Ctrl+Pexpands - Permission flow:
a/d/allow-session/allow-workspace + a config rules engine to auto-resolve - Diff workflow:
a/rapply/rejectfile_diffparts inline - Session resume:
Ctrl+Zclean detach → localdetached.json→gact attach/gact resume→ back where you were. 11 UX surfaces (header chip, sidebar↩marker, sidebardfilter, dashboard column,gact info,gact detached, etc.) all read the same registry source of truth.
CLI (~70 subcommands, see docs/FEATURES.md
for the full list)
gact ask <sid>,gact send,gact wait,gact rungact log <sid> --role assistant --grep "error" --since 1h --format jsongact follow <sid> --role assistant --grep pattern(tail-f)gact dashboard --sort newest --detached-only --format jsongact detached --probe --prune-dead(registry management)gact grep <query> --role user(cross-session full-text)gact dump-bundle(diag + metrics + sessions + detached.json for bug reports)gact completion bash|zsh|fish
Plugin loader under ~/.config/gact/plugins/<name>/plugin.json
Run everything locally:
make test # emulator + tui + conformance + every adapter
make test-race # same, under -raceCurrent coverage (from go test -cover):
| Module | Coverage |
|---|---|
tui/internal/ui |
67.6% |
tui/internal/config |
77.5% |
tui/internal/plugins |
72.9% |
tui/internal/client |
39.5% |
emulator/internal/events |
87.3% |
emulator/internal/scenario |
83.3% |
emulator/internal/store |
72.3% |
emulator/internal/server |
67.5% |
All tests pass. Screenshot-driven UI changes re-record via vhs tapes
in tui/screenshot_*.tape — the .claude/skills/tui-screenshot.md
workflow is documented.
tui/— the Bubbletea client + ~70 CLI subcommandsemulator/— reference backend (boots in ~50ms, no deps)contract/— SPEC.md + the conformance suite every adapter must passadapters/— Claude Code (Go + Python), OpenCode, Crush, Goose bridgesdocs/FEATURES.md— exhaustive reference: every keybinding, CLI command, capability matrix, theme, voice plumbing
Conventional commits (feat:, fix:, test:, docs:, chore:,
refactor:). Run make test-race before pushing. UI changes need a
fresh screenshot in screenshots/ — see
.claude/skills/tui-screenshot.md.
MIT — see LICENSE.






