From c21a4ebd001ca305d862b5390f090f2ef14163cf Mon Sep 17 00:00:00 2001 From: t Date: Tue, 16 Jun 2026 19:28:54 -0600 Subject: base site: claude design plus some tweaks --- .thumbnail | Bin 0 -> 4320 bytes .../_adherence.oxlintrc.json | 425 +++++ .../_ds_bundle.js | 1878 ++++++++++++++++++++ .../_ds_manifest.json | 1 + .../readme.md | 144 ++ .../styles.css | 13 + .../tokens/base.css | 113 ++ .../tokens/colors.css | 87 + .../tokens/fonts.css | 19 + .../tokens/spacing.css | 72 + .../tokens/typography.css | 66 + _shared/components.css | 160 ++ _shared/docs.css | 202 +++ _shared/docs.js | 85 + assets/logo-mark-mono.svg | 10 + assets/logo-mark.svg | 15 + cli.html | 35 + configuration.html | 80 + index.html | 39 + keybindings.html | 31 + lua.html | 66 + research/notes.md | 393 ++++ 22 files changed, 3934 insertions(+) create mode 100644 .thumbnail create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_ds_bundle.js create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_ds_manifest.json create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/readme.md create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/styles.css create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/base.css create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/colors.css create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/fonts.css create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/spacing.css create mode 100644 _ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/typography.css create mode 100644 _shared/components.css create mode 100644 _shared/docs.css create mode 100644 _shared/docs.js create mode 100644 assets/logo-mark-mono.svg create mode 100644 assets/logo-mark.svg create mode 100644 cli.html create mode 100644 configuration.html create mode 100644 index.html create mode 100644 keybindings.html create mode 100644 lua.html create mode 100644 research/notes.md diff --git a/.thumbnail b/.thumbnail new file mode 100644 index 0000000..12df967 Binary files /dev/null and b/.thumbnail differ diff --git a/_ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json b/_ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json new file mode 100644 index 0000000..d745f07 --- /dev/null +++ b/_ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json @@ -0,0 +1,425 @@ +{ + "plugins": [ + "react", + "import" + ], + "rules": { + "react/forbid-elements": [ + "warn", + { + "forbid": [] + } + ], + "no-restricted-imports": [ + "warn", + { + "patterns": [ + { + "group": [ + "components/code/**", + "components/core/**", + "components/forms/**", + "components/navigation/**", + "ui_kits/docs/**", + "ui_kits/home/**" + ], + "message": "Import design-system components from 'index.js', not component internals." + } + ] + } + ], + "no-restricted-syntax": [ + "warn", + { + "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", + "message": "Raw hex color — use a design-system color token via var()." + }, + { + "selector": "Literal[value=/\\b\\d+px\\b/]", + "message": "Raw px value — use a design-system spacing token via var()." + }, + { + "selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:IBM Plex Mono|Saira Condensed))/i]", + "message": "Font not provided by the design system. Available: IBM Plex Mono, Saira Condensed." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|dot|children|key|ref|className|style|children)$/]", + "message": " doesn't accept that prop. Declared props: variant, dot, children." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute[name.name='variant'] > Literal[value!=/^(?:neutral|danger|success|ink|outline)$/]", + "message": " variant must be one of 'neutral' | 'danger' | 'success' | 'ink' | 'outline'." + }, + { + "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|block|icon|iconRight|as|children|key|ref|className|style|children)$/]", + "message": " +panto/docs +CLI + + + + +
+ +
+

Reference

Command line

panto dispatches on its first argument. A handful of subcommands (lua, bootstrap, sessions, models, auth, help) short out before the agent loop; anything else — or nothing — starts the agent in the current directory.

CommandDoes
pantoStart a new conversation in the current directory.
panto --resume [<id>]Resume the most recent session, or one by id prefix.
panto sessionsList saved sessions for this directory.
panto models syncFetch models.dev and rebuild the base models.toml.
panto auth …Inspect and manage auth sessions (status / login / logout).
panto bootstrap [--force]Run the luarocks bootstrap and exit.
panto lua [args…]Drop into the embedded Lua interpreter.
panto helpPrint the usage summary (also --help, -h).

Starting a session

pantoStart a new conversation in the current directory.
panto --resume [<id>]Resume a previous session.

With no flag, panto opens a fresh session. --resume on its own reattaches the most recent session in this directory; --resume <id> resumes the session whose id begins with <id>. Unknown arguments are tolerated (and ignored with a warning), so passing flags panto doesn’t recognise won’t stop it from starting.

Needs a terminal

The UI runs in raw mode and requires an interactive tty on stdin/stdout. Piping input or redirecting output will refuse with an error rather than half-start.

panto sessions

panto sessionsList saved sessions for the current directory.

Sessions are grouped per working directory. Each line is the short id (the first eight hex characters of the session’s UUIDv7), the creation time trimmed to the minute, and the message count:

panto · sessions
$ panto sessions
7f3a9c2b 2026-06-12 09:41 24 messages
b81e0d44 2026-06-11 17:03 6 messages
a0c7f190 2026-06-10 22:18 41 messages

When a directory has no sessions yet, panto prints no sessions for <cwd>. Use any unambiguous id prefix from this list with panto --resume.

panto models

panto models syncFetch models.dev and rebuild the base models.toml.

sync is the only action. It fetches the models.dev catalog, walks the providers in your merged [providers] config, and for each one cross-checks the catalog against the models that provider actually serves — then writes the result to the base-layer models.toml ($PANTO_HOME/models.toml, else $XDG_DATA_HOME/panto/models.toml). Aliases, wire names, context windows, token caps, and pricing come along for the ride. A provider maps onto a catalog entry via model_catalog_name (falling back to the provider name); providers with no matching entry — or no usable synced models — are skipped.

panto · models sync
$ panto models sync
synced 42 model(s) across 3 provider(s) to ~/.local/share/panto/models.toml

Because it writes the base layer, your own user/project models.toml entries always layer on top and win. Re-run it whenever you want to refresh the catalog after providers ship new models.

panto auth

Manage the credentials behind your providers. With no action, panto auth defaults to status.

panto auth status
List every configured auth session and its state. api_key sessions read resolved or unresolved (key/env missing); oauth_device sessions show whether you’re logged in and roughly when the access token expires.
panto auth login <name>
Run the OAuth device flow for the named session. panto prints a verification URL and a user code, then stores (and, if configured, exchanges) the token. An api_key session has nothing to log in to.
panto auth logout <name>
Forget the stored token set for the named session.
panto · auth status
$ panto auth status
anthropic_api api_key resolved
openai_api api_key unresolved (key/env missing)
github_copilot oauth_device logged in (access expires in ~58m)

Auth sessions themselves are declared in config under [auth.<name>]. The provider that uses a session names it with auth = "<name>".

panto bootstrap

panto bootstrap [--force]Run the luarocks bootstrap, then exit.

The bootstrap pipeline prepares the embedded Lua runtime: it stages headers and the base agent tree, materialises the default config, and installs panto’s pinned batteries under $PANTO_HOME. The agent runs the very same pipeline on startup, so you rarely need this by hand — it’s here for first-run setup on a clean machine and for scripted / CI installs. Repeat runs no-op quickly.

--force
Wipe the per-Lua-version rocks tree ($PANTO_HOME/rocks/lua-X.Y.Z/) before reinstalling everything from scratch.

panto lua

panto lua [args…]Drop into the embedded Lua interpreter.

This is panto’s bundled Lua standalone (the same lua.c the agent embeds), with the luarocks runtime already bootstrapped — so require("luarocks.*") and any rocks installed under $PANTO_HOME resolve exactly as they do inside a session. Arguments after lua are passed straight through to the interpreter. Use it to experiment, or to drive luarocks to install extra rocks for your extensions:

shellbash
# run a script with panto’s Lua + rocks tree
+panto lua my-script.lua
+
+# install an extra rock into panto’s tree via the embedded luarocks
+panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'

Slash commands

Inside a session, any line that begins with / is a slash command rather than a prompt — it is handled locally and never sent to the model. The first word (after the slash) is the command name; the trimmed remainder is passed as its arguments.

/compactbuiltin
Compact the conversation now — summarise older turns to reclaim context while keeping a verbatim recent tail. (Compaction also runs automatically; see [compaction].)

/compact is the only built-in. Every other slash command comes from a Lua extension via panto.ext.register_command — see Lua extensions → register_command. An unrecognised command reports an error instead of reaching the model.

Environment

panto reads a few environment variables — for credentials and for locating its files. Configuration values can also pull from the environment via ${env:VAR} substitution (see Configuration).

VariableEffect
ANTHROPIC_API_KEYConsumed by the default Anthropic provider.
OPENAI_API_KEYConsumed by the default OpenAI provider.
PANTO_HOMEOverride the runtime / rocks-tree location. Also where the base config.toml and agent tree are staged.
PANTO_SESSION_DIROverride the base sessions directory. Defaults to $XDG_DATA_HOME/panto/sessions (or ~/.local/share/panto/sessions).
XDG_CONFIG_HOMELocates the user config: $XDG_CONFIG_HOME/panto/ (else ~/.config/panto/).
XDG_DATA_HOMELocates the base config + data: $XDG_DATA_HOME/panto/ (else ~/.local/share/panto/).
+ +
+ + + + \ No newline at end of file diff --git a/configuration.html b/configuration.html new file mode 100644 index 0000000..1823478 --- /dev/null +++ b/configuration.html @@ -0,0 +1,80 @@ + + + + + +panto — Configuration + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Configuration

TOML

panto is configured by layered config.toml files, plus a separate models.toml for model aliases and pricing. Define providers, pick a default model, and gate tools and extensions — everything you specify, nothing you don’t.

Files & layers

Four files are read and merged, lowest precedence first. Missing files are skipped silently.

LayerPathRole
base$PANTO_HOME/config.toml · else $XDG_DATA_HOME/panto/config.tomlAuto-generated by bootstrap. Lowest precedence.
user$XDG_CONFIG_HOME/panto/config.toml · else ~/.config/panto/config.tomlYour personal, machine-wide settings.
project./.panto/config.tomlChecked-in, per-project settings.
local./.panto/config.local.tomlPersonal per-project overrides — git-ignore this. Highest precedence.

Merge semantics: tables merge recursively, but scalars and arrays from a higher layer overwrite wholesale. A project file that sets tools.deny replaces the array entirely — it doesn’t append to a user-level list. Tables accumulate, so a provider defined only at the base layer survives even when a higher layer adds another.

config.tomltoml
# ~/.config/panto/config.toml  (user layer)
+
+[defaults]
+model = "anthropic:sonnet"          # <provider>:<alias>
+
+[providers.anthropic]
+style    = "anthropic_messages"
+base_url = "https://api.anthropic.com"
+auth     = "anthropic_api"          # draws its key from [auth.anthropic_api]
+
+[auth.anthropic_api]
+key = "${env:ANTHROPIC_API_KEY}"    # resolved at load
+
+[tools]
+deny = ["std.bash"]                 # allow every tool except the shell

The local layer

./.panto/config.local.toml is meant to be git-ignored: apply your own overrides on top of the checked-in project layer without committing them.

[providers.<name>]

A provider is pure transport: an API shape and a base URL, plus the name of the auth session that supplies its credential. The chosen name is what you reference on the left of a provider:alias model.

stylestringrequired
The wire protocol: anthropic_messages, openai_chat, or openai_responses.
dialect"codex"
A sub-dialect for styles that share a protocol family. Only style = "openai_responses" accepts it today, with the single value "codex" (the Codex Responses variant).
base_urlstringrequired
The API base URL requests are sent to.
authstringrequired
Names the [auth.<name>] session that supplies this provider’s credential. (Provider-level api_key is no longer accepted — auth lives in one place.)
prompt_cachebooldefault true
anthropic_messages only. Place one advancing cache_control breakpoint on each request. Ignored for other styles.
extra_headerstable
A table of static string headers merged onto every model request (see below).
model_catalog_namestring
Maps this provider onto a models.dev catalog key (e.g. "github-copilot") for panto models sync. Defaults to the provider name.
config.tomltoml
[providers.copilot]
+style    = "openai_chat"
+base_url = "https://api.individual.githubcopilot.com"
+auth     = "github_copilot"
+
+[providers.copilot.extra_headers]
+Copilot-Integration-Id = "vscode-chat"
+X-Initiator            = "user"

Unresolved providers vanish

If a provider’s api_key auth session resolves to empty (its env var isn’t set), the provider is dropped — export the key to bring it back. OAuth providers always survive and resolve at turn time.

[auth.<name>]

A named auth session supplies a credential. Its type is inferred when omitted: a key implies api_key; a client_id implies oauth_device.

Substitution

Every auth value supports ${…} substitution. ${env:VAR} reads the environment; ${name} reads another key in the same section (handy for a shared domain). An unresolved reference becomes the empty string.

API-key sessions

keystringrequired
The API key — usually "${env:VAR}", but a literal works too. Resolved eagerly at load; an empty result leaves the session unresolved.
type"api_key"
Optional; inferred from the presence of key.

OAuth device sessions

client_idstringrequired
The OAuth client id.
device_code_urlstringrequired
Endpoint that issues the device + user codes.
token_urlstringrequired
Endpoint polled for the access token.
dialect"token" | "codex"default token
Flow dialect. codex additionally requires device_poll_url.
scopestring
Requested OAuth scopes.
verification_urlstring
Override the URL shown to the user.
device_poll_urlstring
Distinct poll endpoint (required for codex).
token_request_format"form" | "json"default form
How the token request body is encoded.
redirect_uri / account_id_jwt_claimstring
Optional flow details.
exchange_urlstring
Enables an optional secondary token exchange. Companions: exchange_method (default GET), exchange_token_path (default token), exchange_expires_path, exchange_base_url_path — JSON paths into the exchange response.
config.tomltoml
[auth.github_copilot]
+client_id       = "Iv1.b507a08c87ecfe98"
+domain          = "github.com"          # an ordinary sibling key…
+device_code_url = "https://${domain}/login/device/code"   # …reused here
+token_url       = "https://${domain}/login/oauth/access_token"
+scope           = "read:user"
+
+# optional secondary token exchange
+exchange_url           = "https://api.${domain}/copilot_internal/v2/token"
+exchange_expires_path  = "expires_at"
+exchange_base_url_path = "endpoints.api"

[defaults]

modelstring
The default model, as "<provider>:<alias>" (exactly one colon, both halves non-empty). The provider must resolve.

Model selection falls through in order: an explicit override (a future --model), then defaults.model, then — if exactly one provider resolved and it has exactly one alias in models.toml — that one. Otherwise panto asks you to set a default.

Tools & extensions

[tools] and [extensions] share the same shape: allow- and deny-lists of glob patterns over dotted names (the standard tools live under the std. namespace — std.read, std.write, std.edit, std.bash).

allowstring[]
Glob patterns. Empty means allow-all (still subject to deny).
denystring[]
Glob patterns. A name is denied if it matches any deny pattern.

A name is permitted when it matches at least one allow pattern (or allow is empty) and matches no deny pattern.

config.tomltoml
[tools]
+allow = ["std.*"]          # only the standard tools
+deny  = ["std.bash"]       # …but never the shell

Conflicts are fatal

The same pattern in both allow and deny for one kind is a hard error — panto refuses to start rather than guess.

[compaction]

panto compacts long conversations automatically; this section tunes it. The manual /compact command uses the same settings.

keep_verbatimintdefault 20000
Token budget for the recent tail kept verbatim when compacting.
modelstring
Optional provider:alias override for the model that writes the summary. Defaults to the active chat model.

models.toml

Model aliases live in $XDG_CONFIG_HOME/panto/models.toml, separate from config.toml. Each entry is keyed [<provider>.<alias>] — the provider matches a [providers.<name>], the alias is the short name you reference. A referenced alias with no entry isn’t an error: it’s used verbatim as the wire name with default knobs.

models.toml is layered exactly like config.toml (base → user → project → local). The base layer is auto-generated — panto models sync fetches the catalog and writes it — so your user and project entries always layer on top and win.

Common keys

modelstring
Wire model id sent to the API. Defaults to the alias.
context_windowint
Total input context window, used for the footer’s context-usage display. Optional.
max_tokensint
Per-request output token cap.
input / output / cache_read / cache_writefloat
Pricing in USD per million tokens. All optional; omitted means unknown.

openai_chat only

reasoningstringdefault default
One of default, off, minimal, low, medium, high.

anthropic_messages only

thinkingstringdefault disabled
disabled, enabled, or adaptive.
effortstringdefault medium
low · medium · high · xhigh · max. Used when thinking = "adaptive".
thinking_budget_tokensintdefault 32000
Reasoning-token budget when thinking = "enabled".
thinking_interleavedbooldefault false
Send the interleaved-thinking beta header (honoured when thinking = "enabled").
api_versionstringdefault 2023-06-01
The anthropic-version header.
models.tomltoml
# ~/.config/panto/models.toml
+
+[anthropic.sonnet]
+model      = "claude-sonnet-4-20250514"   # wire name; defaults to the alias
+max_tokens = 8192
+thinking   = "enabled"
+thinking_budget_tokens = 16000
+input  = 3.0        # USD per million tokens
+output = 15.0
+cache_read  = 0.30
+cache_write = 3.75
+
+[openai.gpt]
+model     = "gpt-4o"
+reasoning = "medium"
+input  = 2.5
+output = 10.0

System prompt

The agent’s system prompt is sourced by convention from Markdown files across the same base → user → project layers (base is $PANTO_HOME/agent, where bootstrap stages the bundled default):

SYSTEM.md
Replaces the system prompt for that layer.
APPEND_SYSTEM.md
Appends to it — for small, additive house rules.
COMPACTION.md
Overrides the summary prompt used during compaction (last layer wins; a built-in default applies otherwise).

On resume, prompt changes are reconciled without rewriting the existing conversation.

+ +
+
+ + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..f8ef07f --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + +panto — Getting started + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Getting started

panto

terminal agentziglua

panto is a small terminal coding agent built on libpanto. A minimal system prompt, a tool set you can swap or switch off, native binaries from Zig, extensions in Lua. This is the operator's reference — flags, configuration, key bindings, and the Lua surface.

Overview

You run panto inside a project directory and talk to a model in a terminal UI. The agent reads and edits files and runs commands through tools — and even the standard tools (read, write, edit, bash) ship as extensions, so any of them can be denied individually.

There are no permission prompts and no hidden machinery: providers are declared in a TOML file, the model is chosen by a provider:alias reference, and anything beyond the core is added as a Lua extension. The rest of these pages document each of those surfaces in turn.

Install

panto builds from source with Zig. Clone the repository and run the Zig build; the result is a single native binary.

shellbash
git clone https://code.tjp.lol/pantograph.git
+cd pantograph
+zig build            # native binary at zig-out/bin/panto

On first use, run the one-time bootstrap. It stages the embedded Lua runtime and installs panto’s pinned luarocks batteries (just luv, the libuv event loop) under $PANTO_HOME:

shellbash
panto bootstrap        # prepare the rocks tree (idempotent)

Where things live

By default panto keeps its runtime and rocks tree in $PANTO_HOME (falling back to $XDG_DATA_HOME/panto), and reads configuration from a layered set of TOML files. See Configuration → Files & layers.

First run

Run panto with no arguments inside a project to start a fresh conversation. The agent loads your config, selects the default model, and opens the terminal UI. Type a message and press Enter to send it.

panto · first run
$ export ANTHROPIC_API_KEY=sk-ant-…
$ cd ~/code/ledger && panto
 
panto v0.1.0
cwd: ~/code/ledger
 
add a --json flag to the stats command
 
I’ll wire a --json branch into the stats command. Let me read it first.
read src/stats.zig
pub fn run(args: Args) !void { + const summary = try compute(args); + try printHuman(summary); +}
edit src/stats.zig (1 edit)
- try printHuman(summary); ++ if (args.json) try printJson(summary) else try printHuman(summary);
 
Done — stats --json now prints a machine-readable summary.
 
anthropic:sonnet   18.4k ctx   2.1k tok   $0.03

panto’s UI needs an interactive terminal (a tty). Sessions are saved per directory, so you can pick up where you left off with panto --resume. The full command surface — resuming, listing sessions, auth, the embedded Lua interpreter — is on the Command line page, and every key is listed under Key bindings.

Authenticate

A provider draws its credential from a named auth session. There are two kinds. The simplest is an API key from the environment — the default OpenAI and Anthropic providers read OPENAI_API_KEY and ANTHROPIC_API_KEY directly:

shellbash
export ANTHROPIC_API_KEY=sk-ant-…
+panto                  # the anthropic provider is now live

The second is an OAuth device flow, for providers that authenticate interactively. Log in once and panto stores (and refreshes) the token for you:

shellbash
panto auth login github_copilot
+panto auth status      # show every session and its login state

If a provider seems missing

An api_key session whose environment variable is empty resolves to nothing, and any provider using it is silently dropped (export the key to bring it back). OAuth providers always survive and resolve at turn time. Auth sessions are declared under [auth] in config.
+ +
+
+ + + \ No newline at end of file diff --git a/keybindings.html b/keybindings.html new file mode 100644 index 0000000..09739d8 --- /dev/null +++ b/keybindings.html @@ -0,0 +1,31 @@ + + + + + +panto — Key bindings + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Key bindings

TUI input

The session UI reads raw keystrokes and turns them into editing actions. These are the keys it recognises today; panto negotiates richer terminal protocols at startup so the important ones (notably Shift+Enter) work wherever the terminal allows.

Editing & cursor

The input box is a multi-line editor. Printable keys insert text (full Unicode), and the usual motion keys move the cursor.

KeyAction
any printable keyInsert the character (multi-byte UTF-8 supported).
/ Move the cursor one character.
Ctrl+ / Ctrl+
Alt+ / Alt+ · Alt+B / Alt+F
Move the cursor by word (terminals vary which form they send).
Ctrl+A / Ctrl+EJump to the start / end of the current line.
Home / EndJump to the very start / end of the whole input.
BackspaceDelete the character before the cursor.
DeleteDelete the character under the cursor.
Ctrl+W · Alt+Backspace · Ctrl+BackspaceDelete the previous word.
Ctrl+UDelete from the cursor back to the start of the line.
pastePasted text is inserted literally as one run (bracketed paste), not interpreted key-by-key.

Submit & newline

KeyAction
EnterSend the message to the model.
Shift+EnterInsert a newline instead of sending (terminal permitting — see below).

Whether Shift+Enter inserts a newline depends on your terminal, because in the bare legacy protocol it is indistinguishable from plain Enter. panto works around this where it can — see Terminal protocol.

Selectors & control

Control keys switch the model and reasoning effort mid-session, collapse tool output, hand off to your editor, interrupt a running turn, and exit. The selectors, tool-collapse, and the Esc interrupt stay responsive even while the agent is mid-turn.

KeyAction
Ctrl+MOpen the model selector.
Ctrl+ROpen the reasoning-effort selector.
Ctrl+OCollapse / expand all tool-call output.
Ctrl+GEdit the current draft in $EDITOR, then read it back.
EscInterrupt the running agent turn; dismiss an open selector.
Ctrl+C / Ctrl+DExit cleanly.

Selectors are live-only

Picking a model (Ctrl+M) or reasoning level (Ctrl+R) rebuilds the active provider config and applies it to the running agent for this session — nothing is written back to config.toml.

Terminal protocol

Two TUI behaviours depend on the terminal’s keyboard protocol. panto negotiates the best available at startup and degrades gracefully.

Shift + Enter

In the bare legacy protocol, Enter and Shift+Enter send the same byte. To tell them apart, panto pushes the Kitty keyboard protocol (disambiguate + report-alternates) and queries the terminal; if that’s unavailable it falls back to xterm’s modifyOtherKeys mode (which tmux and xterm honour).

TerminalShift + Enter
Kitty, Ghostty, foot (Kitty protocol)Newline — fully supported.
xterm, tmux (modifyOtherKeys)Newline — via the fallback.
macOS Terminal.app (neither)Indistinguishable from Enter — submits.

No newline in Terminal.app

On terminals that support neither protocol, Shift+Enter can’t be distinguished from Enter, so it submits. Use a terminal with the Kitty protocol (Ghostty, Kitty, foot) — or tmux — if you want multi-line input.

Bracketed paste

panto enables bracketed paste, so multi-line pastes arrive as a single literal block rather than being re-interpreted as a stream of keypresses (a stray newline in a paste won’t submit your message early).

What isn’t here yet

The input model reserves space for richer behaviour — key-release events and super/hyper modifiers under the full Kitty protocol — but those aren’t consumed today, so they’re not bindings yet.
+ +
+
+ + + \ No newline at end of file diff --git a/lua.html b/lua.html new file mode 100644 index 0000000..7baeba0 --- /dev/null +++ b/lua.html @@ -0,0 +1,66 @@ + + + + + +panto — Lua extensions + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Lua extensions

require("panto")

Everything beyond the core is a Lua extension — including the standard tools. An extension is a Lua file that calls into the panto.ext table to add tools, slash commands, and UI behaviour. This is how panto stays small without staying useless.

Overview

Inside an extension, local panto = require("panto") gives you the API. The panto.ext table has three entry points:

register_toolmodel-facing
Add a tool the model can call.
register_commandyou-facing
Add a /slash command to the REPL.
on / emitUI events
Hook the UI event bus to wrap or replace how things render.

All extensions share one long-lived Lua interpreter, so module-global state persists across calls. The runtime includes luv (libuv), so tool handlers can perform async I/O.

Loading extensions

Drop a .lua file into an extensions directory and it loads at startup. panto discovers extensions across three layers:

LayerLocation
base$PANTO_HOME/agent
user$XDG_CONFIG_HOME/panto/extensions/ · else ~/.config/panto/extensions/
project./.panto/extensions/

Project shadows user shadows base. Which extensions and tools actually load is gated by the [extensions] and [tools] allow/deny policies.

Name collisions abort startup

If two surviving extensions register a tool (or command) with the same name, panto refuses to start rather than pick one silently. A Lua command may not shadow a builtin like /compact.

register_tool

A tool is something the model can call. Describe it with a JSON-Schema for its arguments and a handler that returns a string result.

namestringrequired
The tool name the model invokes.
descriptionstringrequired
Shown to the model; explain when to use it.
schematablerequired
A JSON-Schema table (type = "object", properties, required) describing the arguments.
handlerfunction(input)
Receives the decoded arguments table; returns a string result. May yield (async).
.panto/extensions/echo.lualua
local panto = require("panto")
+
+panto.ext.register_tool {
+  name = "echo",
+  description = "Echo back the given message.",
+  schema = {
+    type = "object",
+    properties = {
+      message = { type = "string", description = "The text to echo back." },
+    },
+    required = { "message" },
+  },
+  handler = function(input)
+    return "echo: " .. input.message
+  end,
+}

register_command

A slash command runs locally when you type /name in the REPL. The handler runs synchronously and acts by side effect (typically writing output); its return value is ignored.

namestringrequired
Command name, without the leading /.
descriptionstringrequired
One-line summary (for listings).
handlerfunction(args)
args is the trimmed text after the command name — the empty string when none was given.
.panto/extensions/greet.lualua
local panto = require("panto")
+
+panto.ext.register_command {
+  name = "greet",
+  description = "Print a greeting. Optional args name who to greet.",
+  handler = function(args)
+    local who = args ~= "" and args or "world"
+    io.write("\n[greet] hello, " .. who .. "!\n")
+  end,
+}

Events: on & emit

panto.ext.on(name, handler) subscribes a handler to the UI event bus — the same bus the built-in TUI uses. Handlers fire in registration order, and can wrap or replace the component that renders a given moment of the transcript.

The handler receives an event object e that is valid only during the call (copy any field you need into a local — don’t read e later at render time):

e.namestring
The event name.
e.<field>
Read-only payload fields (nil when absent, so you can branch on them).
e:getComponent()
The current native component, as an opaque pass-through handle.
e:setComponent(c)
Replace the component for this boundary — pass the handle back to wrap it, or a Lua component table to replace it.

The events and the payload fields they carry:

EventPayload fields
toolindex, tool_name, id, delta, input, output
thinkingindex, delta, text
assistant_textindex, delta, text
user_messagetext
session_startversion, cwd, model
compactionsummary
custom— produced by emit
.panto/extensions/skill-badge.lualua
local panto = require("panto")
+
+-- Change how the "skill" tool renders in the transcript.
+panto.ext.on("tool", function(e)
+  if e.tool_name ~= "skill" then return end   -- ignore every other tool
+  local name = e.tool_name                     -- snapshot now: e is short-lived
+  e:setComponent({
+    render = function(self, width)
+      return { "▸ running skill: " .. name }
+    end,
+  })
+end)

panto.ext.emit(name, data) fires a custom event on the same bus, running both native and Lua handlers. Today data is surfaced as an opaque custom payload.

Lua components

A Lua component is a table you hand to e:setComponent. The one required method is render:

renderfunction(self, width)required
Return an array of strings — one per line. Runs synchronously (it may not yield), each line is truncated to width columns, and any error becomes a safe fallback line so the frame never crashes. An empty array is valid (zero lines).
handleInputfunction(self, data)
Optional. Handle input routed to the component.
firstLineChanged / invalidatefunction
Optional render-cache hooks.

Runtime & luarocks

panto embeds a pinned Lua and luarocks, and ships exactly one battery: luv (libuv’s event loop), the single runtime dependency. It drives panto’s coroutine scheduler and gives extensions one rich async-I/O surface. The bootstrap reconciles the installed rocks tree against panto’s manifest on every startup — installing what’s missing, removing what’s stale.

Anything else is yours to add. Install extra rocks into panto’s tree with the embedded luarocks, via panto lua:

shellbash
panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'

One-time setup

On a fresh machine, run panto bootstrap once to build the rocks tree before your extensions need it.
+ +
+
+ + + \ No newline at end of file diff --git a/research/notes.md b/research/notes.md new file mode 100644 index 0000000..2cfcadc --- /dev/null +++ b/research/notes.md @@ -0,0 +1,393 @@ +# panto — verified content notes (from repo source) + +Source: https://code.tjp.lol/pantograph.git/ (cgit). Only document IMPLEMENTED features. +Project self-describes as "Early". NOT implemented yet (DO NOT DOCUMENT): server mode, subagents, +MCP, permission systems, AGENTS.md automation, skills, customizable /prompts, native (.so) extensions +(planned "next"; Lua is what ships). Standard tools (read/write/edit/bash) ship as extensions. + +## CLI invocation / subcommands (src/subcommand.zig — printHelp + dispatch, VERBATIM help text) + +panto Start a new conversation. +panto --resume Resume the most recent conversation in this directory. +panto --resume Resume the conversation whose id begins with . +panto sessions List saved sessions for this directory. +panto auth status Show configured auth sessions and login state. +panto auth login Log in to an OAuth auth session (device flow). +panto auth logout Forget a stored OAuth token. +panto bootstrap [--force] Run the luarocks bootstrap and exit. +panto lua [args...] Drop into the embedded Lua interpreter. +panto help | --help | -h Show help. + +Details: +- dispatch routes argv[1]; unknown/absent -> agent REPL. +- `lua`: embedded standalone Lua interpreter (panto's lua.c build) with luarocks runtime bootstrap + completed first, so `require("luarocks.*")` and the configured rocks tree work the same as in the + agent process. argv rewritten so the interpreter sees `lua [...args]` (affects arg[0]). +- `bootstrap`: runs the luarocks runtime bootstrap pipeline only, then exits before any agent loop. + First-run setup on a fresh machine (downloads+compiles batteries, stages headers, materializes + config); also good for CI/scripted installs. Idempotent: re-runs no-op fast. + `--force`: wipe the per-Lua-version tree before reinstalling everything. Equivalent to deleting + `$PANTO_HOME/rocks/lua-X.Y.Z/` by hand then running `panto bootstrap`. +- `sessions`: lists sessions for the current working directory. One per line: + ` messages`. short-id = first 8 hex chars of the session + UUIDv7. created trimmed to `YYYY-MM-DD HH:MM`. "no sessions for " when empty. +- `auth` (default action = status): + - status: per configured auth session. + api_key -> "resolved" OR "unresolved (key/env missing)" + oauth_device -> "logged in (access expires in ~Nm)" OR "not logged in (run: panto auth login )" + - login : OAuth device flow. Prints: "To authorize, open this URL in a browser: " and + "enter the code: ". Runs secondary token exchange now if configured. api_key sessions: + "'' is an api_key session; nothing to log in to". + - logout : deletes the stored token set ("logged out of ''" / "no stored token for ''"). + +## Config files (TOML, merged base -> user -> project) [from help text] +- $XDG_DATA_HOME/panto/config.toml (base; auto-generated) +- $XDG_CONFIG_HOME/panto/config.toml (user) +- ./.panto/config.toml (project) +Schema teaser (NEED config_file.zig for full schema): +- [providers.] define providers +- [defaults] model = ":" pick the default model +- [tools] / [extensions] allow/deny globs gate tools & extensions +- Model aliases (wire name, reasoning, max_tokens, pricing) live in models.toml. + +## Environment variables [from help text] +- OPENAI_API_KEY, ANTHROPIC_API_KEY — consumed by the default providers. +- PANTO_SESSION_DIR — override base sessions dir. Default $XDG_DATA_HOME/panto/sessions + or ~/.local/share/panto/sessions. +- PANTO_HOME — override the runtime/rocks tree location. + +## Slash commands (src/command.zig) +- REPL treats any line beginning with `/` as a slash command (never sent to the model). +- Parse: first whitespace-delimited word after `/` = name; trimmed remainder = args. +- Unknown command -> error.CommandNotFound -> user-facing error message. +- Builtin command seen: `/compact` (registers from its own module; compacts the conversation). +- Lua extensions add commands: a script calls `panto.ext.register_command { name, description, handler }`, + harvested by lua_runtime.zig; main.zig registers each. Lua-backed commands carry a lua_ref. +- Tab-completion: planned (registry has list()). + +## CONFIG FILE — full schema (src/config_file.zig, VERIFIED) + +### Layers — 4 TOML files, merged lowest precedence first (NOTE: help text only lists 3; loader has 4) +1. base — $PANTO_HOME/config.toml OR ${XDG_DATA_HOME:-$HOME/.local/share}/panto/config.toml (auto-generated by bootstrap) +2. user — ${XDG_CONFIG_HOME:-$HOME/.config}/panto/config.toml +3. project — ./.panto/config.toml +4. local — ./.panto/config.local.toml (intended to be git-ignored; personal overrides on top of team project layer) +Missing files are skipped silently. + +### Merge semantics +- Tables merge recursively; scalars AND arrays from a higher layer overwrite WHOLESALE (no array append). + e.g. project `tools.deny = [...]` replaces the array entirely. +- Tables accumulate: a provider defined only at base survives when project adds a different provider. + +### `${...}` substitution (auth values) +- `${env:VAR}` reads the environment; `${sibling}` reads another key in the SAME [auth.] section. +- Unresolved reference (missing env var / sibling) → empty string. e.g. define `domain = "github.com"` then + `device_code_url = "https://${domain}/login/device/code"`. + +### [providers.] (networked provider; transport only) +- style (required) — one of: openai_chat | anthropic_messages | openai_responses +- base_url (required) — string +- auth (required) — names the [auth.] session supplying the credential. + (Clean break: provider-level api_key / api_key_env_var are NO LONGER accepted.) +- prompt_cache (bool, default true) — anthropic_messages ONLY; one advancing cache_control breakpoint per + request. Ignored for openai_chat. +- [providers..extra_headers] — table of string header name=value, merged onto each model request. +- A provider whose api_key auth resolves empty is DROPPED ("export the key or the provider disappears"). + OAuth providers always survive (resolved at turn time / via `panto auth login`). + +### [auth.] (named auth session; `type` inferred if omitted: client_id→oauth_device, key→api_key) +api_key: + - type = "api_key" (optional; inferred from `key`) + - key = "${env:VAR}" (usually) or a literal — resolved eagerly; empty ⇒ session unresolved. +oauth_device: + - type = "oauth_device" (optional; inferred from `client_id`) + - dialect = "token" (default) | "codex" + - client_id (required) + - device_code_url (required) + - token_url (required) + - device_poll_url (required ONLY for dialect="codex") + - verification_url (optional) + - scope (optional) + - token_request_format = "form" (default) [enum panto.TokenRequestFormat] + - redirect_uri (optional) + - account_id_jwt_claim (optional) + - secondary token exchange (flat keys; active when exchange_url present): + exchange_url (enables exchange) + exchange_method (default "GET") + exchange_token_path (default "token") — JSON path to the token + exchange_expires_path (optional) — JSON path to expiry + exchange_base_url_path (optional) — JSON path to a dynamic base_url + - arbitrary sibling keys allowed (e.g. `domain`) for use in ${...} templating. + +### [defaults] +- model = ":" — provider must resolve. Selection precedence: + 1) future --model override, 2) defaults.model, 3) if exactly ONE provider resolved AND it has exactly ONE + alias in models.toml, use that, 4) else error (no model selected). +- Model ref format: exactly one colon, both halves non-empty (e.g. `anthropic:claude-sonnet-4-6`). + +### [tools] / [extensions] — allow/deny glob policy (identical shape) +- allow = [glob,...] deny = [glob,...] +- Empty allow ⇒ allow-all (still subject to deny). Permitted iff matches ≥1 allow (or allow empty) AND no deny. +- Same pattern in BOTH allow and deny (same kind) ⇒ hard error. +- Names are dotted, e.g. std.read, std.write, std.edit, std.shell; globs like `std.*`. + (Standard tools ship as extensions under the `std.` namespace and can be denied individually.) + +### [compaction] +- keep_verbatim = 0> — kept-suffix token budget (omitted ⇒ libpanto default). +- model = ":" — optional override model used for compaction (provider must resolve). + +### Model aliases live in models.toml (resolved separately). Knobs seen in buildProviderConfig: +- common: wire `model` name, `reasoning` (ReasoningEffort, default .default), `max_tokens` (default 64000) +- anthropic_messages extra: api_version (default "2023-06-01"), thinking (default disabled), + effort (default medium), thinking_budget_tokens (default 32000), thinking_interleaved (default false) + -> NEED models_toml.zig for the exact TOML table layout + key names. + +### Model aliases live in models.toml (src/models_toml.zig, VERIFIED) +Path: ${XDG_CONFIG_HOME:-$HOME/.config}/panto/models.toml. Missing file ⇒ empty registries (no error). +A referenced alias with NO entry is NOT an error: the alias is used verbatim as the wire model name, +with default knobs and unknown pricing (zero-config convenience). + +Entry table key: [.] — matches a [providers.] in config.toml; + is the short name referenced as : (e.g. anthropic:sonnet). + +Keys: +- model = wire model id sent to the API; defaults to if omitted. +- max_tokens = per-request OUTPUT token cap; >0 else null (= provider/library default). +- Pricing (all optional, USD per MILLION tokens; omitted = UNKNOWN/null, write `= 0` for known-zero): + input = + output = + cache_read = + cache_write = +- openai_chat ONLY: + reasoning = default | off | minimal | low | medium | high (default: default) +- anthropic_messages ONLY: + api_version = Anthropic-Version header; default "2023-06-01" + thinking = disabled | enabled | adaptive (default: disabled) + effort = low | medium | high | xhigh | max (default: medium) + — only used when thinking = "adaptive" + thinking_budget_tokens = max reasoning tokens for thinking="enabled"; default 32000; + null ⇒ max_tokens − 1; ignored when adaptive/disabled + thinking_interleaved = send interleaved-thinking beta header (default false); + only honoured when thinking = "enabled" + +Example models.toml: + [anthropic.sonnet] + model = "claude-sonnet-4-20250514" + max_tokens = 8192 + thinking = "enabled" + thinking_budget_tokens = 16000 + input = 3.0 + output = 15.0 + cache_read = 0.3 + cache_write = 3.75 + + [anthropic.opus] + model = "claude-opus-4-8" + thinking = "adaptive" + effort = "high" + + [openai.gpt] + model = "gpt-4o" + input = 2.5 + output = 10.0 + +## LUA RUNTIME / LUAROCKS (src/manifest.zig + subcommand.zig, VERIFIED) +- panto embeds a PINNED Lua + luarocks and ships "batteries" (rocks). The ONLY battery auto-installed is + `luv` (libuv event loop) — the single runtime dependency. It drives panto's coroutine scheduler and + gives extension authors one rich async I/O surface. +- Bootstrap reconciles the installed rocks tree against the manifest on EVERY startup: installs missing, + removes stale. The battery list is compiled into the binary; users do NOT configure it. +- Extra rocks are the user's responsibility. Install via: + panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command(...)' + (A higher-level `panto rocks install` is PLANNED — DO NOT document.) +- Per-Lua-version rocks tree lives at $PANTO_HOME/rocks/lua-X.Y.Z/ (wiped by `panto bootstrap --force`). +- luarocks MAJOR.MINOR.PATCH-REV version strings (e.g. "1.52.1-0") passed straight to luarocks install. + +## EXAMPLES INVENTORY (repo /examples) +- examples/extensions/echo.lua (632) — example panto Lua EXTENSION <-- fetch +- examples/extensions/greet.lua (719) — example panto Lua EXTENSION <-- fetch +- examples/tools/ (dir) — example tools <-- fetch tree +- examples/simple-agent.lua (3110) — libpanto Lua SDK agent (NOT panto-ext; for libpanto, skip-ish) +- examples/simple-agent-go/ — libpanto Go SDK agent (NOT panto-ext) + +## LUA EXTENSION SURFACE — verified from examples +Module: `local panto = require("panto")` (available inside extensions AND in `panto lua`). + +### panto.ext.register_tool { name, description, schema, handler } (examples/extensions/echo.lua) +- name : tool name (string) the model calls. +- description : shown to the model. +- schema : JSON-Schema table — { type="object", properties={...}, required={...} }. +- handler : function(input) -> string. `input` is the decoded arguments table; return a string result. + echo example: handler = function(input) return "echo: " .. input.message end + +### panto.ext.register_command { name, description, handler } (examples/extensions/greet.lua) +- Registers a `/`-slash command in the REPL. name is WITHOUT the leading `/`. +- handler = function(args) ... end. `args` is the trimmed text after the command name ("" if none). + Handlers run SYNCHRONOUSLY and act by side effect (e.g. io.write); the RETURN VALUE IS IGNORED. + greet example: `/greet` or `/greet ` -> io.write("\n[greet] hello, "..who.."!\n") +- A Lua command name colliding with a builtin (e.g. compact) is rejected (DuplicateCommand). + +### Where extensions live (from greet.lua header comment) +- Project: `./.panto/extensions/` +- User: the user config dir's `extensions/` dir (i.e. ${XDG_CONFIG_HOME:-~/.config}/panto/extensions/). +- Drop a `.lua` file there; it's loaded at startup. Gate via [extensions] allow/deny globs in config.toml. + +### Runtime +- The embedded interpreter is the panto `lua.c` standalone (Lua + luarocks). `luv` (libuv) is always + available for async I/O; panto's coroutine scheduler drives the libuv loop. +- `panto lua [args...]` drops into the same interpreter for scripting/experiments; + `panto bootstrap` prepares the rocks tree. +- NOTE: I have register_tool + register_command verified. The file src/lua_event_bridge.zig (event hooks) + is NOT yet read — DO NOT invent event APIs (on_turn, hooks, etc.) until verified. + +## KEYBINDINGS — verified from src/tui_input.zig (the input decoder; P1 scope, "minimal but correct") +The REPL input layer turns raw stdin into keys. Keys it recognises TODAY (document only these): + +Editing / text: +- Printable characters (full UTF-8, multi-byte) -> insert +- Enter (Return) -> submit the message +- Shift+Enter -> insert a newline (terminal-dependent, see below) +- Backspace -> delete char before cursor +- Delete -> delete char under/after cursor +- Tab -> tab +- Bracketed paste -> pasted text inserted literally as one run + (not interpreted key-by-key) + +Cursor / navigation (decoded with modifiers): +- Left / Right -> move cursor by character +- Ctrl+Left / Ctrl+Right -> move by word (the "word-motion path") +- Home / End -> line start / end +- Up / Down -> arrow keys (decoded; move within a multi-line draft) +- Page Up / Page Down -> decoded +- Alt+ -> decoded (ESC-prefixed alt forms) + +Control: +- Ctrl+C -> interrupt / quit +- Ctrl+D -> EOF / exit +- Esc -> escape +- Ctrl+ generally (0x01–0x1a) -> decoded as ctrl+a..z + +Shift+Enter detail (genuinely useful doc): in the bare legacy protocol Enter and Shift+Enter both send `\r` +and are indistinguishable. At startup panto negotiates the **Kitty keyboard protocol** (pushes flags 1|4 = +disambiguate + report-alternates; deliberately NOT report-events) and queries the terminal; if confirmed it +reads Shift+Enter as `CSI 13;2u`. Otherwise it falls back to xterm **modifyOtherKeys mode 2** (tmux/xterm), +which sends `CSI 27;2;13~`. On terminals supporting NEITHER (e.g. macOS Terminal.app) the two stay +identical and Enter submits — there's no newline binding there. (Ghostty maps shift+enter to a bare `\n`.) +Deferred / negotiated-but-not-consumed (DO NOT present as features): key-release events, super/hyper +modifiers, full Kitty disambiguation — modelled for later phases, not active. + +## MAIN / STARTUP / TUI WIRING (src/main.zig, VERIFIED) — lots of new facts + +### Agent-mode flags (the ONLY ones) +- `--resume` → resume most recent session in this cwd. +- `--resume ` → resume session whose id has prefix . +- No flag → new session. Unknown args are tolerated/ignored (warn). +- TUI REQUIRES an interactive tty ("panto's TUI requires an interactive terminal"). + +### Builtin slash commands +- ONLY builtin registered is from compaction.zig → `/compact` (manual compaction). + Everything else is added by Lua extensions. (Do NOT invent /help, /model, etc. as slash commands.) + +### LIVE keybindings added by main.zig (the interactive ones!) +- Ctrl+C / Ctrl+D → clean exit (UserExit). +- Ctrl+M → MODEL selector (runtime model picker overlay). +- Ctrl+R → REASONING-effort selector (runtime reasoning picker overlay). + Both are live-session only: a pick rebuilds the provider config and pushes it to the agent + (setConfig); NOTHING is written back to config.toml. +- Enter submits; Shift+Enter newline (see tui_input notes). Input box + footer + transcript differential render. + +### Lua extension surface — EXPANDED (main.zig wiring, VERIFIED) +- `require('panto')` is wired to the native `panto.so` module PLUS the CLI's `ext` subtable → `panto.ext`. +- Lua runtime is ONE long-lived lua_State; module-global state persists across calls. Registers with the + agent as a single ToolSource named `panto-lua`. +- `luv` (libuv) scheduler is installed BEFORE extensions load, so tool handlers may YIELD (async I/O). +- panto.ext.register_tool {…} → adds a model-callable tool (see echo.lua). +- panto.ext.register_command {…} → adds a `/`-slash command (see greet.lua). +- panto.ext.on(...) → registers a UI EVENT handler into the App's event bus, in registration order. + Extensions can WRAP/REPLACE built-in TUI components this way (e.g. a `tool_details` handler replacing + the default `tool (?)` component). Superseded overrides are released (ref + RenderCache freed). +- panto.ext.emit(...) → a Lua call that drives the SAME event bus. + (NOTE: exact event/kind names + on/emit signatures live in src/lua_event_bridge.zig — fetching now. + Until verified, describe on/emit as the component-override/event surface and cite tool_details as the + one named example from main.zig. Don't enumerate events I haven't seen.) + +### Extension discovery (3 layers; src/extension_loader.discoverAndLoad) +- base = $PANTO_HOME/agent +- user = $XDG_CONFIG_HOME/panto (or $HOME/.config/panto) +- project = ./.panto +- Precedence: project shadows user shadows base. Tool-NAME collisions across surviving entries ABORT startup. +- Gated by [extensions] and [tools] allow/deny policies from config.toml. +- (greet.lua header: drop a .lua under .panto/extensions/ project, or the user config's extensions/ dir.) + +### System prompt — a real config surface (system_prompt.zig, referenced by main.zig) +- The agent's system prompt is sourced by convention from SYSTEM.md / APPEND_SYSTEM.md across the + base/user/project layers (base = $PANTO_HOME/agent, where bootstrap stages the bundled SYSTEM.md). + SYSTEM.md replaces; APPEND_SYSTEM.md appends. Reconciled on resume without rewriting history. +- Compaction system prompt = COMPACTION.md across layers (last wins; built-in default otherwise). + +### Compaction +- Automatic compaction is armed at startup. [compaction].keep_verbatim default = 20000 tokens. +- `/compact` triggers it manually. [compaction].model can override the model used for compaction. + +### Sessions / auth (confirm) +- Sessions are per-cwd; created on demand. Resume by id-prefix or latest. "resumed session <8hex> ( messages)". +- Per-turn auth resolution: api_key is a no-op; oauth_device refreshes/exchanges or runs an interactive + device login before the turn. models.toml path = $XDG_CONFIG_HOME/.config/panto/models.toml. +- Config error messages are friendly (e.g. "defaults.model must look like \"provider:model\"."). + +## panto.ext EVENT SURFACE — VERIFIED (src/lua_event_bridge.zig) + +### panto.ext.on(name, handler) — subscribe a Lua handler to a UI event +- The handler participates in the SAME native EventBus the built-in TUI fires. Registration order matters. +- handler = function(e) ... end. `e` is a bridged EVENT OBJECT, valid ONLY during the handler call + (snapshot any field you need into a local — do NOT close over `e` and read it at render time). +- Event object API: + e.name -> the event name (string) + e. -> read-only payload fields (nil when absent/empty, so you can branch: + `if e.tool_name then ...`) + e:getComponent() -> the current (native default) component, as an OPAQUE passthrough handle + e:setComponent(c) -> replace the component for this boundary. `c` is EITHER that passthrough + handle (pass-through / wrap) OR a Lua component table (replace). +- Payload fields by event family (from pushPayloadField): + tool : index(int), tool_name, id, delta, input, output (lifecycle: tool_delta has delta, + tool_result has output, tool_details…) + thinking : index, delta, text + assistant_text : index, delta, text + user_message : text + session_start : version, cwd, model + compaction : summary + custom : (no structured fields — produced by emit) + (VERIFIED event NAME used in tests: "tool". Lifecycle boundary names tool_delta/tool_result/tool_details + appear in source comments. Present these as the event families; the canonical example is "tool".) + +### A Lua COMPONENT is a table: + { render = function(self, width) -> { "line1", "line2", ... } end, + handleInput = function(self, data) ... end, -- optional + firstLineChanged = ..., -- optional (cache-derived by default) + invalidate = ... } -- optional +- render() MUST return an array of strings; runs SYNCHRONOUSLY (may NOT yield — unlike tool handlers); + each line is truncated to `width` columns; any error yields a safe "[lua component error: …]" fallback + line (the frame never crashes). Empty array {} = zero lines (valid). + +### Three override patterns (from the bridge's own tests): +- pass-through native default: e:setComponent(e:getComponent()) +- replace with a Lua component: e:setComponent({ render = function(self,w) return {"hi"} end }) +- claim-by-name (the canonical extension shape): + panto.ext.on("tool", function(e) + if e.tool_name ~= "skill" then return end -- ignore everything else + local name = e.tool_name -- snapshot at handler time + e:setComponent({ render = function(self, w) return { "SKILL:"..name } end }) + end) + +### panto.ext.emit(name, data) — fire a custom event on the SAME bus +- Runs native AND Lua handlers for `name`. `data` is currently surfaced only as an opaque `.custom` + payload (structured marshalling is future). A component chosen from a bare emit is NOT auto-mounted yet. + +## ALL CONTENT GATHERED — ready to build. (lua_runtime/extension_loader internals not needed beyond above.) + +## KEY MODEL (src/tui_key.zig, VERIFIED) — what the P1 decoder currently handles +Decoded keys the running TUI populates today (the model is bigger for future phases, but ONLY these are live): + printable chars, enter, backspace, arrows (up/down/left/right), home, end, escape, Ctrl+C, Ctrl+D. +Mods modelled: ctrl, alt, shift, super, hyper — but P1 decoder only ever sets ctrl (super/hyper need Kitty +protocol; left false). Key events: press/repeat/release — most terminals only emit .press. +=> The keybindings page must document ONLY the live subset; note richer keys are planned, not shipped. -- cgit v1.3