# panto CLI/TUI: gaps and proposals Survey of the current CLI/TUI surface (2026-07-03) and what to do about it. Framing first, because it decides everything below: panto's job is not to do everything, it's to make everything possible. It is a minimal interactive shell over libpanto plus a powerful Lua extension API, in the spirit of pi (https://pi.dev) — users build their own ideal agent on top, and that agent need not be a coding agent at all (the shipped coding tools are themselves deny-able Lua extensions). So every "gap" lands in one of three buckets: - **Core** — makes the shell itself usable and scriptable. Baseline, not coding-specific. Ships in Zig (and should generally also be reachable from Lua). - **Extension API** — core shouldn't do the thing; core should make the thing *buildable*. New API surfaces are co-designed with Travis before building. - **Not ours** — coding-agent conveniences. Belongs in `../panto-agent` or other out-of-tree extensions; listed only so we stop re-proposing it. Priorities within a bucket: **P0** = bug or table-stakes, **P1** = next tranche, **P2** = deliberate deferral. --- ## 1. Bugs (core, P0, all small) Fix regardless of anything else below. 1. **Duplicate short session IDs.** `panto sessions` shows `id[0..8]` (`src/subcommand.zig:374`), but IDs are UUIDv7 and the first 8 hex chars are the top 32 bits of the millisecond timestamp — no randomness until char 15. Two sessions created within ~65 s in one project display identical short IDs. Fix: shortest-unambiguous prefix per listing (extend past 8 while any two collide). 2. **`AmbiguousSessionId` exits silently.** `--resume ` matching two files propagates the error to a bare `exit(1)` at `src/main.zig:282-283` — no message. Only `SessionNotFound` gets a tidy error (`main.zig:724-727`). Fix: print the candidates and exit. 3. **`--resume` (bare) and `panto sessions` disagree on "most recent".** `findMostRecentSession` takes the lexicographic filename max = newest-*created* (`file_system_jsonl_store.zig:981-1010`); the listing sorts by newest-*modified*. Fix: make `latest()` use `modified`. 4. **`trimCreated` doesn't trim.** Promises `YYYY-MM-DD HH:MM`, returns `iso[0..16]` with the `T` intact (`src/subcommand.zig:386-394`). 5. **`--resume` flag peeking can't rewind.** A `-`-leading token after `--resume` is warned-and-dropped instead of parsed as its own flag (`main.zig:676-688`). Becomes real once a second agent flag exists (§2). 6. **Base config template comment says `[tools]`** where the code reads `[extensions]` (`luarocks_runtime.zig:460` vs `config_file.zig:201`). --- ## 2. Core: command-line flags Today the entire agent-mode flag surface is `--resume []` (`main.zig:658-697`). For an "interactive agent SDK" the bar is: launch it, point it at a model, script it, resume it. That's about five flags. ### P0 - **`-p, --print ` — one-shot non-interactive mode.** One agent turn (tools and all), text to stdout, meaningful exit code; prompt from the arg or stdin when piped. panto currently *requires* a tty (`main.zig:494-503`) and cannot be scripted at all — the biggest gap in the product. Composes with `--resume` for headless continuation. Pairs with libpanto's "one-shot simple API" todo (`docs/todos.md:12`). - **`-m, --model `.** Already anticipated: `selectModel(model_override)` exists and `main.zig:227` passes `null`. Wire it through; accept a bare alias when unambiguous. - **`--version`.** Embed at build time; print `panto `. - **Unknown flags error, don't warn-and-continue** (`main.zig:686,693`). A typo silently launching a TUI is how scripts hang. ### P1 - **`-c, --continue`** — ergonomic alias for bare `--resume`. - **`--no-extensions`** — skip the Lua runtime: fast `-p` in CI, and the escape hatch when an extension breaks startup. Given that even the shipped tools are extensions, define semantics as "built-ins only, policy-denied everything else". - **`--effort `** — same pattern as `--model`. ### Skip - `--config ` — the 4-layer TOML merge already covers it. - `--output-format json` for `-p` — wait for a real consumer. - `--cwd` — `cd && panto`. ### `panto sessions` output (P0) Currently `<8-char-id> messages`, while the useful fields (`last_user_message`, `model`, `modified`) are computed by `buildFileInfo` and discarded. Proposal: ``` ID MODIFIED MSGS MODEL LAST MESSAGE 0197c2a4 2026-07-01 14:32 18 anthropic:sonnet fix the segfault in tui_engine when… 0197b1f0 2026-06-30 09:15 4 openai:gpt add a --version flag ``` Sort and *show* by modified; shortest-unique IDs (§1.1); truncate last message to width; plain aligned columns, no box-drawing. Print the sessions dir path as the last line so the storage is discoverable. Keep resume as a flag (it composes with `-p`/`-m`; `sessions` stays list-only) — the real contract is that listed IDs paste into `--resume` without ambiguity. --- ## 3. Core: slash commands Current inventory: **`/compact`** (`src/compaction.zig:13`). The framework (registry, Lua `panto.ext.register_command`, capture-to-transcript, error rendering) is complete — and mostly-empty is correct for panto. But a shell whose product *is* its extension points still has to be self-describing and steerable. These are baseline and coding-agnostic, so they're core Zig; each should also be achievable from Lua via `panto.ext.agent` (the sufficiency audit in §7). ### P0 - **`/help`** — registered commands (`Registry.list()` exists solely for this and tab completion) plus the key bindings. Today nothing in the product reveals that `/compact` or Ctrl+G exist. For an extensions-first tool, discoverability of what's currently loaded is not a convenience — it's how users see their own extensions. - **`/quit`** — trivial; the only exit today is Ctrl+C/D. - **`/model [provider:alias]`**, **`/reasoning [level]`** — no arg opens the existing selector; with arg sets directly. Makes the selectors discoverable and terminal-agnostic (see the Ctrl+M problem, §4). - **`/new`** — fresh session in place; today you quit and relaunch. ### P1 - **`/resume []`** — in-TUI session switcher; no arg opens a fuzzy `Selector` over sessions (short-id + modified + last message rows). - **`/status`** — provider, model, reasoning, session id, message count, context used vs window. All already in `command.Context` or computable. ### Completion and typeahead (P1) The fuzzy machinery already exists and is good — the model selector does case-insensitive subsequence filtering with Ctrl+N/P + Enter. Extend the same feel to the input box (`docs/todos.md:22` tracks this): - **Leading-`/` typeahead**: when the buffer starts with `/`, surface a live-filtered command list (from `Registry.list()`, which exists for exactly this) in the same selector idiom; Tab or Enter-on-selection completes. Registered Lua commands appear automatically — this is another discoverability channel for extensions, like `/help`. - **Tab completes file/dir names from cwd** anywhere in the buffer: complete the token under the cursor against the filesystem, common-prefix first, selector on ambiguity. Coding-agnostic (paths are universal). - Later: **per-command argument completion** declared by the command (e.g. `/model` completing `provider:alias`, Lua commands supplying a completion function). That's an extension API surface — §7. --- ## 4. Core: key bindings Input box editing is solid (word motion, Ctrl+A/E/U/W, Shift+Enter multiline, bracketed paste). Gaps: ### P0 - **Input history (Up/Down recall).** Decoded and dropped today (`tui_components.zig:1207`). Up at buffer-top / Down at buffer-bottom navigate history so multiline arrow-editing still works; in-memory per-run is enough to start. - **Idle Escape clears the input buffer.** Makes Escape uniformly "cancel the thing in progress" (it already interrupts turns and closes selectors). - **Fix Ctrl+M.** Ctrl+M *is* carriage return; the model-selector binding only exists on Kitty-protocol terminals and silently vanishes on plain xterm/tmux. With `/model` (§3) as the discoverable path, move the chord to a legacy-safe key (Ctrl+T is free) or accept slash-only on legacy terminals — either way `/help` documents reality. ### P1 - **Ctrl+L** — clear/redraw (engine already has full-redraw machinery). - **Buffer typed input during a turn** and restore it to the input box after; full mid-turn steering waits on libpanto queueing (`docs/todos.md:15`). - **Dedup the chord dispatch**: Ctrl+O/M/R are re-implemented in both `handleBytes` (`tui_app.zig:1801-1842`) and `pumpTurnKeys` (`tui_app.zig:2286-2304`). One `appChord(key) -> ?Action` both call. This is also the seam the keybinding extension API (§7) slots into. ### Deliberate non-features - Kill-ring/yank, undo — already deferred in-code ("plan P2"); agreed. - In-app transcript scrolling — engine delegates to native terminal scrollback by design (`tui_engine.zig:36-42`); keep. --- ## 5. Core: config.toml settings The `[defaults]`/`[compaction]`/`[extensions]`/`[providers]`/`[auth]` sections cover the agent; the TUI itself has zero settings. A halfway decent agent TUI supports at least: ### P1 - **`[defaults] reasoning = ""`** — today reasoning comes only from models.toml per-alias or the Ctrl+R selector; a session default belongs next to `defaults.model`. - **`[tui] editor = "..."`** — Ctrl+G honors only `$EDITOR`; a config override is the conventional courtesy. - **`[tui] tools_collapsed = true|false`** — the Ctrl+O collapse state's starting value. - **`[tui] theme`** — the palette is deliberately comptime-fixed today (`tui_theme.zig:5-6` says centralization, not theming). A minimal version: named color overrides for the existing `StyleName` slots. P1 only if cheap; a full theming system is P2 at best. ### Skip - Keymap section — subsumed by the keybinding extension API (§7). - Footer layout config — subsumed by the footer segment API (§7). - Anything per-command/per-tool — that's extension config, which extensions can already read themselves (Lua reads files). --- ## 6. Core: the footer Today the footer is one dim line: `provider:alias (reasoning)`, latest context tokens, session tokens, session cost (`tui_components.zig:1533`, all pushed from Zig via `setModel`/`setContextTokens`/...). Worth adding: ### P1 - **Context as a fraction, not a raw count.** `12.3k ctx` is meaningless without the window; models.toml already has `context_window`. Show `12.3k/200k ctx` or a percentage — this is the number that tells you when compaction looms, arguably the footer's whole job. - **Session short-id** — pairs with `/status` and `panto sessions`; makes "which session am I in" free. - **Transient key-hint slot** — e.g. `esc interrupt` while a turn runs. One hint at a time, context-dependent; not a permanent hint bar. ### P2 / skip - git branch, cwd — coding/shell-flavored and knowable from the terminal; an extension can add these once the segment API (§7) exists. Skip in core. --- ## 7. Extension API gaps (make it possible) The survey surfaced these as missing Lua surfaces. Per AGENTS.md, each gets co-designed before building — this list is scope, not spec. - **Keybinding registration.** Wanted: extensions can bind chords to Lua handlers (and presumably rebind/shadow core ones). Blocked on the `appChord` central dispatch (§4) existing first. Design questions for the co-design pass: conflict policy vs core chords, per-context bindings (idle vs during-turn vs selector), and whether declared bindings auto-appear in `/help`. - **Footer segments.** The footer is entirely Zig-fed today; extensions have no way to add a readout (git branch, active skill, queue depth). Likely shape: register a segment that returns a short string, repainted on demand — rhymes with the existing screen-component API. - **Completion providers.** Per-command argument completion for Lua slash commands (§3): a command optionally supplies a completion function. - **Session store access** from Lua (list/resolve/load). Needed for an extension to build its own `/resume`-like or archival behaviors; today sessions are invisible to extensions. - **Selector/prompt UI for extensions.** TUI screen components are done (`docs/todos.md:31`); a fuzzy-pick-from-list primitive is the missing interactive piece extensions keep needing (model pickers, skill pickers, session pickers all rhyme). Not a gap (previously misreported here): the current agent and conversation **are** exposed — `panto.ext.agent` wraps the live session agent with the full method surface (`lua_runtime.zig:219-242`), and the conversation hangs off it. What's worth auditing instead is whether that surface is *sufficient* for §3-parity from Lua: model/config switching, starting a fresh session, reading usage/config for a `/status`-alike. --- ## 8. Not ours (so we stop re-proposing it) - **`/usage`, `/cost`** — buildable today-ish from the Lua usage-metrics API + models.toml pricing; panto-agent material. - **`/init`, `/review`, `/pr`, rules/skills injection** — coding-agent features; panto-agent already owns this space (`agent.rules`, `agent.skill`). - **`/undo` / checkpointing** — real design work and coding-flavored; out-of-tree when someone wants it. - **User keymap config file** — subsumed by the keybinding extension API (§7); a TOML keymap section can be revisited if non-extension users ask. --- ## 9. Sequencing 1. §1 bug fixes + `panto sessions` output + shortest-unique IDs — one PR, pure fixes. 2. `--model`, `--version`, strict flag parsing — small PR. 3. `-p/--print` — the headline; with libpanto's one-shot todo or a thin plain-text presenter over the existing loop. 4. Slash commands P0 (`/help`, `/quit`, `/model`, `/reasoning`, `/new`). 5. Keybindings P0 (history, idle-Escape, Ctrl+M move) + `appChord` dedup. 6. Footer P1 (context fraction, session id) + config.toml `[defaults] reasoning` / `[tui]` basics. 7. `/resume` switcher, `/status`, completion/typeahead (§3). 8. Extension API co-design round: keybindings, footer segments, completion providers, session store, selector primitive (§7) — then panto-agent picks up §8.