A shared visual layer for Bubble Tea CLIs — one palette, layout primitives, and an opt-in agent PromptBox behind a small capability contract, so every tool built on it looks and feels like it came from the same dev.
Built for and extracted from code (a
launcher for oh-my-pi) and the atyrode
cockpit in atyrode/dotfiles, which remain
its reference consumers.
go get github.com/atyrode/cli-kit
| Package | What it gives you | Pulls in |
|---|---|---|
clikit (root) |
palette & styles, layout/panel/meter/workspace primitives, the PromptBox, Run, the capability contract |
lipgloss, Bubble Tea, bubbles |
clikit/omp |
Ask/Act backends that shell out to a headless oh-my-pi run | stdlib only (omp binary needed at runtime) |
clikit/ollama |
an Act backend speaking to a local ollama daemon over loopback HTTP | stdlib only (daemon needed at runtime) |
The backends are deliberately separate packages: importing the core never buys
you a model runner. Nothing in this library installs, launches, or assumes an
LLM — a tool that wants the PromptBox opts in by implementing an interface, and
a tool that wants a local model opts in by importing clikit/ollama and
bringing its own daemon.
- Palette & styles (
palette.go) — colour tokens (CAcc,CBord, …), theMeterRamp, text-presentation glyphs (GWarn/GBroken/GReset), and shared lipgloss styles (StDim,StHead, …). - Layout and section helpers (
layout.go,meter.go) —PadLeft/Pad,WindowList(a clipped, scrollbar'd column),Scrollbar, meters, and full-widthRule/SeparatedSectionsboundaries. Widths are terminal cells; empty sections produce no orphan rule. - Workspace and panel primitives (
workspace.go,panel.go) —WorkspaceNavowns ordered selection, wrapping next/previous navigation, and stable application-defined IDs;Panel,PanelContentWidth, andClipLinesprovide shared rounded chrome with terminal-cell clipping and no wrap-driven footer displacement. - Footer conventions (
palette.go,layout.go) —NewHelpapplies the shared key/description/separator palette to Bubble Help, whileWrapHelpwraps complete required cues without dropping them. Both remain ANSI-aware through lipgloss width measurement. - The PromptBox (
promptbox.go) + the consumer contract (contract.go,run.go) +ParseActions(actions.go), the tolerant model-output → action-set parser every Act backend shares.
A CLI built on cli-kit is a plain Bubble Tea model (tea.Model). It launches
through clikit.Run, which mounts capabilities the model opts into by
implementing small interfaces — structural, compile-time, à la carte:
| Implement | Get |
|---|---|
| (nothing extra) | palette + layout + the footer/help conventions |
Askable → Asker() Asker |
the PromptBox in Ask mode (read-only Q&A) |
Commandable → Commander() Commander |
the PromptBox in Act mode (propose → live-preview → keep/revert) |
Documented → Docs() DocCorpus |
grounding injected into the backend's system prompt |
clikit.Run(app) detects what's implemented and mounts the box behind a toggle
key (default ctrl+o). The same key or esc closes it and cancels any
in-flight request. Act mode takes precedence over Ask when both are present.
Options: WithToggleKey, WithAltScreen, WithMouseCellMotion,
WithMessageFilter.
type helpApp struct{ /* your tea.Model */ }
func (helpApp) Asker() clikit.Asker { return omp.NewAsker(myDocs) }
func (helpApp) Docs() clikit.DocCorpus { return myDocs }
func main() { _, _ = clikit.Run(helpApp{}, clikit.WithAltScreen()) }omp.NewAsker shells out to a headless oh-my-pi run (omp -p --mode text --no-session --no-tools --model claude-haiku-4-5 --append-system-prompt <docs> <prompt>) and streams the answer; dismissing the box kills the subprocess. Set
Asker.ReplaceSystem for a narrow grounded assistant that must replace the
coding-agent prompt and its managed scaffolding. Override the evaluator via
Asker.Model.
Implement clikit.Commander — two-phase so the box can show the model working:
type Commander interface {
Propose(ctx context.Context, prompt string) (<-chan string, error) // stream raw output
Parse(output string) ([]Action, error) // completed output → typed actions
}The box streams Propose, runs Parse, and emits ActionsProposedMsg — the
host applies the actions immediately (live preview) and snapshots its prior
state. enter emits ActionsConfirmedMsg (keep; it carries the original
prompt, so a launcher can forward it), esc emits ActionsRevertedMsg
(restore the snapshot). A host whose applied set differs from the parsed
proposal (derived or repaired changes) sends AppliedActionsMsg back so the
box's "applied" list stays truthful. Apply actions through the same code path as a manual
change — the closed Action{Key, Value} set is your tool's agent-facing API
surface.
Backends: omp.Commander (headless oh-my-pi turn, scaffolding stripped) or
ollama.Commander (local daemon, temperature 0, JSON-constrained output). Both
parse with clikit.ParseActions, which tolerates prose around the JSON object.
A backend that also implements clikit.Loadable (as ollama.Commander does)
lets the box offer a load/unload toggle (ctrl+l) and show residency state —
the model occupies RAM only when the user chooses, and a one-off suggestion
never silently leaves weights resident.
The original design record (rationale, the runtime-mutation wall, phasing) is
kept as DESIGN-promptbox.md — a historical document;
where it disagrees with the code, the code wins.
go build ./... && go test ./...
Plain Go module, no code generation, no CGO. CI runs gofmt, go vet, and the
test suite.