Zolt is a minimal Zig TUI chat client inspired by OpenCode and Codex.
This is not ready for production use.
Right now, Zolt is a test project to see how quickly a harness can be built in Zig for experimentation and testing.
It focuses on a fast single-pane workflow:
- stream responses in-terminal
- switch providers/models quickly
- keep multi-conversation history
- use picker-style UX for
/commands,/model, and@file references - render Markdown responses clearly in-chat (headings, lists, quotes, code fences, inline code)
Zolt is a barebones, hackable AI coding/chat TUI with a small codebase in Zig. It is intentionally lightweight but keeps the UX patterns that matter for daily use.
- Single-pane chat UI with vim-like modes (
normal/insert) - Streaming responses
- Markdown-aware chat rendering (headings, lists, block quotes, fenced code, inline code)
- Multiple saved conversations (persisted to disk)
- Codex-style conversation preview title from first prompt (for untitled/new chats)
- Manual
/compactplus auto-compaction when context window is low - Provider + model selection from
models.devcache - Context window footer (
used/fulland% leftwhen available) - Slash command popup (Codex-style picker)
/modelpopup picker@file popup picker@pathfile content injection into prompt context- Clipboard image paste into
@pathreferences (Ctrl-Vor/paste-image) - Skill discovery/injection from Codex/OpenCode-style directories (
$skill-name,/skills,SKILLtool) - Tool loop with discovery/edit/exec primitives:
READ(allowlisted shell read commands)LIST_DIR,READ_FILE,GREP_FILES,PROJECT_SEARCHAPPLY_PATCH,EXEC_COMMAND,WRITE_STDIN,WEB_SEARCH(DuckDuckGo default, Exa optional),VIEW_IMAGE,SKILL,UPDATE_PLAN,REQUEST_USER_INPUT(non-blocking inline)
- Runtime/crash logging with
/logscommand and size-based log rotation (*.log.1)
- Zig
0.15.2(or very close) - Interactive terminal (TTY) for
zoltchat mode - Auth for at least one provider:
- provider API key env var, or
- OpenAI Codex/ChatGPT subscription auth via
CODEX_HOME/auth.json(or~/.codex/auth.json)
- Build:
zig build- Run:
zig build runPass CLI flags through Zig with --:
zig build run -- -h
zig build run -- -s <conversation-id>
zig build run -- run "explain src/main.zig"CLI helpers:
zolt -h
zolt --help
zolt --version
zolt models
zolt models opencode
zolt models --provider openai
zolt models --provider openai --search codex
zolt models --provider openai --select --search codex
zolt models --provider openai --set-default gpt-5.3-codex
zolt -s <conversation-id>
zolt --session <conversation-id>
zolt run "<prompt>"
zolt run --session <conversation-id> "<prompt>"Use zolt run when another tool/script needs a one-shot answer on stdout (no TUI).
zolt run "<prompt>"
zolt run --session <conversation-id> "<prompt>"
zolt run --provider openai --model gpt-5-chat-latest "<prompt>"
zolt run --session <conversation-id> --provider openai --model gpt-5-chat-latest "<prompt>"
zolt run -s <conversation-id> "<prompt>"
zolt run --output text "<prompt>"
zolt run --output logs "<prompt>"
zolt run --output json "<prompt>"
zolt run --output json-stream "<prompt>"Notes:
zolt runuses the same model/provider selection as normal mode.- Pass
--providerand--modelto force an explicit run-scoped selection. --modelrequires--provider.--sessionresumes that conversation context first, then appends your prompt.--outputcontrols formatting:text: final assistant response only (default)logs: tool call/result log lines + final responsejson: one JSON object with metadata, final response, stable token usage, and captured eventsjson-stream(ndjson/jsonlalias): newline-delimited JSON events while running
jsonoutput always includes:usage.prompt_tokens(number|null)usage.completion_tokens(number|null)usage.total_tokens(number|null)
jsonoutput also includeserror:nullon success- object on failure with
code,message,retryable,source
- Existing top-level fields remain unchanged:
provider,model,session_id,prompt,response,events. usageis normalized across providers (input_tokens/output_tokensmap toprompt_tokens/completion_tokens).- Tool loop is enabled in run mode (
READ,LIST_DIR,READ_FILE,GREP_FILES,PROJECT_SEARCH,APPLY_PATCH,EXEC_COMMAND,WRITE_STDIN,WEB_SEARCH,VIEW_IMAGE,SKILL,UPDATE_PLAN,REQUEST_USER_INPUT). - In
textmode, stdout is only the final assistant response (tool call placeholders are not returned as final output).
- Install to
~/.local(puts binary at~/.local/bin/zolt):
zig build install -Doptimize=ReleaseFast --prefix "$HOME/.local"To build with the experimental vaxis backend path enabled:
zig build install -Doptimize=ReleaseFast -Dvaxis=true --prefix "$HOME/.local"If ~/.local/bin is in your PATH, you can then run:
zolt- Set auth (examples):
export OPENAI_API_KEY=...Or for OpenAI Codex subscription auth, run Codex login so this file exists:
$CODEX_HOME/auth.json(ifCODEX_HOMEis set)- fallback:
~/.codex/auth.json
If you logged into OpenCode OAuth/Codex plugin, Zolt also reads:
$XDG_DATA_HOME/opencode/auth.json- fallback:
~/.local/share/opencode/auth.json
Zolt reads provider env vars from models.dev provider metadata when present, plus local fallbacks.
Common keys:
OPENAI_API_KEYOPENCODE_API_KEYOPENROUTER_API_KEYANTHROPIC_API_KEYGOOGLE_GENERATIVE_AI_API_KEYorGEMINI_API_KEYZENMUX_API_KEYEXA_API_KEY(only forWEB_SEARCHwhenengine:"exa"is requested)
Use /provider <id> [auto|api_key|codex] to switch provider/auth mode, then /model to pick models.
For provider openai, Zolt also supports Codex/ChatGPT subscription auth without OPENAI_API_KEY:
- Reads token data from
CODEX_HOME/auth.jsonor~/.codex/auth.json - Reads OAuth token data from OpenCode auth file (
$XDG_DATA_HOME/opencode/auth.jsonor~/.local/share/opencode/auth.json) - Uses bearer
tokens.access_token - Sends
ChatGPT-Account-IDwhen available - Routes requests to
https://chatgpt.com/backend-api/codex(/responses) - If no subscription token exists, switching to OpenAI codex auth via
/providerwill triggercodex login.
If both subscription auth and OPENAI_API_KEY are present, API key auth is used first.
Optional auth preference override:
ZOLT_OPENAI_AUTH=auto(default): API key first, then Codex auth fallbackZOLT_OPENAI_AUTH=api_key: API key onlyZOLT_OPENAI_AUTH=codex: Codex auth first (fallback to API key)
Zolt looks for an optional config file at:
$XDG_CONFIG_HOME/zolt/config.jsonc- fallback:
~/.config/zolt/config.jsonc
Supported keys:
provider(alias:default_provider_id,selected_provider_id)model(alias:default_model_id,selected_model_id)theme:codex|plain|forestui(alias:ui_mode):compact|comfycompact_mode:true/false(legacy alias forui)openai_auth(alias:openai_auth_mode):auto|api_key|codexauto_compact_percent_left(alias:auto_compact_trigger_percent_left): integer0..100(default15)keybindings(alias:hotkeys): optional key overrides for normal/insert mode
Example:
Accepted key values for keybindings:
- single character, like
"q"or"H" - control combos
"ctrl-a"through"ctrl-z" - named keys:
"esc","enter","tab","backspace","space","up","down","pgup","pgdn"
Zolt pulls provider/model data from:
https://models.dev/api.json
This cache includes model context window metadata used in the footer.
Commands:
/modelsshows cache status/models refreshis recommended regularly to keep latest OpenAI models frommodels.dev- CLI model listing:
zolt modelsshows provider IDs + model countszolt models <provider-id>prints exact model IDs (plus config snippet)zolt models --provider <id> --search <query>filters models by id/namezolt models --provider <id> --select [--search <query>]opens a numbered picker in terminal and saves defaultszolt models --provider <id> --set-default <model-id>writes provider/model defaults to config without opening TUI
Global / stream-time:
Ctrl-Zsuspend Zolt (resume with shellfg)Ctrl-Cquit Zolt immediately- While streaming:
Esc Escinterrupts generation PgUp/PgDnscroll chat history (works in both normal and insert modes)Ctrl-Popens command palette quick actions
Normal mode:
ienter insert modeaenter insert mode and move cursor right by oneqquitjscroll down chat historykscroll up chat historyhmove input cursor leftlmove input cursor rightxdelete character at cursorH/Lshift conversation strip left/right/enter insert mode and start slash command inputCtrl-Popen command palette
Insert mode:
Escreturn to normal mode (or close active picker)Entersend prompt (or accept active picker selection)Tabaccept active picker selectionBackspacedelete character before cursorCtrl-Vpaste image from clipboard into input as@pathCtrl-Popen command paletteCtrl-N/Ctrl-Pmove picker selection down/upUp/Downarrows move picker selection up/down
Picker triggers:
/opens slash command picker/helpopens command palette/commandsopens command palette>opens command palette/modelopens model picker@opens file picker
/help(same as/commands)/commands/provider(shows current provider and OpenAI auth mode options)/provider [id] [auto|api_key|codex](OpenAI auth mode)/model [id]/models [refresh]/files [refresh]/logs [runtime|crash](show log paths and file size/status)/skills [name|refresh](list, inspect, or reload discovered skills)/new [title]/sessions [id](no id opens conversation picker)/compact(compact current conversation now)/compact auto [on|off](toggle auto-compaction near context limit)/title <text>/theme [codex|plain|forest]/ui [compact|comfy]/quit/q(alias of/quit)/paste-image
-Dvaxis=truebuilds use the vaxis backend by default.- ANSI backend remains only as a deprecated fallback path when built without vaxis.
- Runtime backend switching via
/ui backend ...is deprecated and removed.
Quick manual checks for startup latency:
Linux:
time -p zolt --help
time -p zolt --versionmacOS:
time zolt --help
time zolt --versionCompare ANSI vs vaxis-enabled builds:
zig build install -Doptimize=ReleaseFast --prefix "$HOME/.local"
time -p zolt --help
zig build install -Doptimize=ReleaseFast -Dvaxis=true --prefix "$HOME/.local"
time -p zolt --helpZolt discovers SKILL.md files using Codex/OpenCode-compatible roots.
Global roots:
$XDG_CONFIG_HOME/opencode/skill/*/SKILL.md(fallback:~/.config/opencode/skill/*/SKILL.md)$XDG_CONFIG_HOME/opencode/skills/*/SKILL.md(fallback:~/.config/opencode/skills/*/SKILL.md)$XDG_CONFIG_HOME/zolt/skill/*/SKILL.md(fallback:~/.config/zolt/skill/*/SKILL.md)$XDG_CONFIG_HOME/zolt/skills/*/SKILL.md(fallback:~/.config/zolt/skills/*/SKILL.md)~/.agents/skills/*/SKILL.md~/.claude/skills/*/SKILL.md~/.zolt/skill/*/SKILL.mdand~/.zolt/skills/*/SKILL.md(legacy/local convenience)$CODEX_HOME/skills/*/SKILL.md(fallback:~/.codex/skills/*/SKILL.md)
Project roots (searched from repo root -> cwd, so deeper paths override):
.opencode/skill/*/SKILL.md.opencode/skills/*/SKILL.md.agents/skills/*/SKILL.md.claude/skills/*/SKILL.md.codex/skills/*/SKILL.md
Yes: skills in the directory where Zolt is currently running are included (if they are under one of the project roots above).
Usage:
- Mention
$skill-namein your prompt to inject that skill’s fullSKILL.mdcontent. - Use
/skillsto list cached skills. - Use
/skills <name>to inspect a specific skill entry. - Use
/skills refreshafter adding/changing skill files.
When you type @path, Zolt can:
- autocomplete via file picker
- inject referenced file contents into prompt context on send
For referenced image files, Zolt injects image metadata (path/mime/size/dimensions) instead of raw binary bytes.
Quoted paths are supported for spaces:
@"docs/My File.md"@'src/other file.zig'
Default XDG paths:
- state:
~/.local/share/zolt/workspaces/<scope>.json - models cache:
~/.cache/zolt/models.json - runtime log:
~/.local/share/zolt/logs/runtime.log - crash report log:
~/.local/share/zolt/logs/crash.log
<scope> is derived from your git root when available (or current directory otherwise),
so conversations are automatically scoped per project/workspace.
If XDG paths are unavailable/unwritable, Zolt falls back to:
<workspace-root>/.zolt/data/workspaces/<scope>.json<workspace-root>/.zolt/cache/models.json<workspace-root>/.zolt/data/logs/runtime.log<workspace-root>/.zolt/data/logs/crash.log
Logging verbosity defaults to info. Set ZOLT_LOG_LEVEL to debug, info, warn, or err to control runtime log volume.
When a log exceeds 2 MiB, Zolt rotates it to runtime.log.1 / crash.log.1 and starts a fresh file.
zig build test
zig build run
zig fmt src/*.zig build.zigNotes:
zig build testalso runs formatting checks viabuild.zig.- main binary name is
zolt.
src/main.zigapp entrysrc/tui.zigTUI and interaction logicsrc/provider_client.zigprovider streaming clientssrc/models.zigmodels.dev cache + catalog parsingsrc/state.zigpersisted conversations and token usagesrc/paths.zigXDG/fallback path handling
Zolt is directly inspired by OpenCode and Codex interaction patterns, especially:
- streaming-first terminal flow
- picker-based model/command/file selection
- concise, keyboard-centric interaction
{ // startup defaults "provider": "openai", "openai_auth": "codex", "auto_compact_percent_left": 12, "model": "gpt-4.1", "theme": "codex", "ui": "compact", "keybindings": { "normal": { "quit": "x", "command_palette": "ctrl-o" }, "insert": { "picker_next": "ctrl-j", "paste_image": "ctrl-y" } } }