- Rust 99.8%
- Shell 0.2%
Drop the stderr logging added for debugging. The run_handler/spawn_handler wrappers stay (they dedup the respond/spawn boilerplate) but no longer log; the panic hook, per-event tracing, and TUG_ACP_LOG gating are gone. |
||
|---|---|---|
| .tug | ||
| benches | ||
| crates/tug-tui | ||
| plans | ||
| scripts | ||
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| tug.deepseek.example.toml | ||
| tug.example.toml | ||
| tug.zai.example.toml | ||
Tug
Tug is a terminal-native coding assistant built in Rust with:
ratatuifor the TUIrig-corefor model and tool calling- OpenAI-compatible and Anthropic provider support
It is designed around a single model per session, streaming responses, local code tools, and a fast keyboard-driven workflow.
Current Capabilities
- OpenAI-compatible chat completions
- OpenAI Codex subscription auth and backend support
- Google Antigravity auth and backend support (Gemini 3, Claude, GPT-OSS sandbox)
- Streaming assistant output
- Multi-turn chat history
- Tool calling for
read,edit,write,grep,find,bash,python3,terminal_*(tmux-backed background sessions), andtask/wait(subagent spawning) - Subagent orchestration via the
tasktool (configurable allowlist and dedicated model per agent) - Agent Client Protocol (ACP) support
- Interruptible runs
- Multiline composer with cursor movement
@file and directory mentions with inline picker- External editor handoff via
$EDITOR - Shell shortcuts:
!commandruns in the user's default shell and adds output to future model context;!!commandruns without adding context - Config file loading with environment overrides
- Session model switching backed by
models.dev - Persisted JSONL sessions with resume support
- Cache usage visibility when the provider reports cached tokens
Install
cargo build
Run Tug with:
cargo run
Configuration
Tug loads configuration from the first available location:
TUG_CONFIG./tug.toml$XDG_CONFIG_HOME/tug/config.toml~/.config/tug/config.toml
Environment variables override file values:
TUG_PROVIDERorTUG_TYPETUG_MODELTUG_THINKINGTUG_BASE_URLorOPENAI_BASE_URLTUG_API_KEYorOPENAI_API_KEY
When no environment overrides are present, Tug also restores the last selected
session target from session.json in the Tug config directory:
$XDG_CONFIG_HOME/tug/session.json~/.config/tug/session.json
That persisted state stores the last selected provider, model, endpoint,
thinking level, active models.dev selector, and the current session file path
when one exists. API keys are not stored there.
Example config:
type = "openai"
model = "gpt-4.1-mini"
thinking = "off"
[openai]
model = "gpt-4.1-mini"
base_url = "https://api.openai.com/v1"
api_key = "replace-me"
[anthropic]
model = "claude-sonnet-4-0"
base_url = "https://api.anthropic.com"
api_key = "replace-me"
[openai_codex]
model = "gpt-5.5"
base_url = "https://chatgpt.com/backend-api"
[models_dev.overrides.openai]
base_url = "https://shared-openai-compatible-endpoint.example/v1"
[models_dev.overrides."openai:gpt-4.1-mini"]
base_url = "https://your-openai-compatible-endpoint.example/v1"
type selects the provider. Supported values:
openaianthropicopenai-codexgoogle-antigravity
For environment overrides:
- OpenAI:
TUG_API_KEYorOPENAI_API_KEY - DeepSeek models selected from the catalog:
TUG_API_KEYorDEEPSEEK_API_KEY - Anthropic:
TUG_API_KEYorANTHROPIC_API_KEY - OpenAI Codex:
TUG_API_KEYor/login - Google Antigravity:
/login antigravity(OAuth only; no env key)
The shipped examples are:
[models_dev.overrides] applies only to models selected from the models.dev catalog.
It does not change your normal configured model, and Tug does not infer that a custom
configured model is a models.dev model.
Provider-wide overrides can use openai, anthropic:, or another supported
catalog provider key such as deepseek: style keys, and exact provider:model
overrides take precedence over provider-wide ones.
Custom providers
Declare your own named providers under [providers.<name>] to expose models that
are not in the models.dev catalog — for example a local llama.cpp server. Unlike
a plain [openai_compatible] block (which only sets the default endpoint), custom
providers are first-class: switch to them in-session with /model set <name>:<model>,
the model picker, or /model search, and the choice persists across restarts.
[providers.gemma]
name = "Local Gemma (llama.cpp)" # optional display name
type = "openai" # wire protocol: openai | anthropic | openai-codex | google-antigravity (default openai)
base_url = "http://127.0.0.1:8080/v1"
api_key = "local" # optional literal key (local servers ignore it but it must be non-empty)
# api_key_env = "GEMMA_API_KEY" # optional: read the key from this env var instead
models = ["gemma-4-26B-A4B"] # model ids this provider exposes
context_window = 65536 # optional, shown in the picker
Then, from any directory where this config applies:
/model set gemma:gemma-4-26B-A4B
A custom provider rides on the wire protocol named by type, so an OpenAI-compatible
server needs no other plumbing. Put the block in ~/.config/tug/config.toml to make
the provider available globally (in any project without its own ./tug.toml). To
start a local server, see scripts/llama-gemma.sh, which
serves Gemma via llama.cpp with -hf/-hfd and MTP speculative decoding.
The built-in system prompt lives in src/prompt.rs.
Skills
At startup, Tug scans these skill directories in order and keeps the first skill
for each duplicate name:
./.tug/skills/~/.tug/skills/~/.agents/skills/~/.claude/skills/~/.config/opencode/skills/~/.codex/skills/~/.pi/agent/skills/
Each skill directory must contain a SKILL.md file with Agent Skills YAML
frontmatter. Tug loads the name, description, and exact SKILL.md path at
startup and appends them to the system prompt as:
<available_skills>
<skill>
<name>skill-name</name>
<description>What the skill does and when to use it.</description>
<location>/absolute/path/to/skill-name/SKILL.md</location>
</skill>
</available_skills>
Tug also instructs the model to use the read tool to load a skill file when
the task matches its description, and to resolve relative paths inside a skill
against the skill directory.
Tug also includes the session working directory and current date in the system prompt for new sessions. When you resume an existing session, Tug reuses the original persisted system prompt instead of regenerating it, so prompt caching is not invalidated by a new date string.
Prompt Templates
Tug supports Pi-style prompt templates. Place Markdown templates in:
./.tug/prompts/~/.config/tug/prompts/
Type / followed by the template name in the composer to expand it before Tug
persists or sends the prompt. review.md becomes /review.
Templates support the same substitutions as Pi:
$1,$2, ... for positional arguments$@or$ARGUMENTSfor all arguments joined with spaces${@:N}for arguments starting at positionN(1-indexed)${@:N:L}forLarguments starting at positionN
Example:
---
description: Review a file
---
Review this file carefully: $1
Typing /review src/main.rs expands to Review this file carefully: src/main.rs.
Z.AI Coding Plan
Tug works with Z.AI through its OpenAI-compatible coding endpoint.
Example local config:
type = "openai"
[openai]
model = "glm-5.1"
base_url = "https://api.z.ai/api/coding/paas/v4"
Then export your key:
export TUG_API_KEY="your-zai-key"
cargo run
The repo’s local tug.toml is set up this way by default.
DeepSeek
Tug works with DeepSeek through its OpenAI-compatible API.
Example local config:
type = "openai"
thinking = "off"
[openai_compatible]
model = "deepseek-chat"
base_url = "https://api.deepseek.com"
Then export your key:
export DEEPSEEK_API_KEY="your-deepseek-key"
cargo run
DeepSeek is also available through /model search deepseek and /model set deepseek:<model>.
The catalog models currently include deepseek-chat, deepseek-reasoner,
deepseek-v4-flash, and deepseek-v4-pro. For deepseek-v4-flash and
deepseek-v4-pro, Tug maps /thinking off to DeepSeek thinking.type = disabled,
/thinking low|medium|high to reasoning_effort = high, and /thinking xhigh
to reasoning_effort = max. Older reasoning models such as deepseek-reasoner
reason by model choice and do not expose an effort control in Tug.
OpenCode Go thinking dialects
opencode-go is an OpenAI-compatible gateway in front of several non-OpenAI
model families, and each family speaks a different thinking dialect. Tug picks
the request shape per model rather than sending OpenAI's nested reasoning
object to all of them (the gateway rejects that with extra parameter: reasoning):
- GLM, MiniMax, MiMo, and Kimi models use the flat chat-completions
reasoning_effort = low|medium|highfield (xhighclamps tohigh). deepseek-v4-proanddeepseek-v4-flashuse the DeepSeek dialect (thinking.typeplusreasoning_effort), so effort control works the same as onapi.deepseek.com.qwen3.*models use Qwen'senable_thinkingtoggle.
Models served through opencode-go's Anthropic-messages endpoint (qwen3.7-max,
qwen3.7-plus, minimax-m3) use the Anthropic thinking controls described
below.
Anthropic
Tug also supports Anthropic’s API directly.
Example config:
type = "anthropic"
model = "claude-sonnet-4-0"
[anthropic]
base_url = "https://api.anthropic.com"
Then export your key:
export ANTHROPIC_API_KEY="your-anthropic-key"
cargo run
Tug also supports /thinking off|low|medium|high|xhigh for Anthropic sessions, plus Ctrl+X then L for the in-app thinking effort picker.
For Claude 4.6 models this maps to Anthropic's native effort plus adaptive
thinking. For older Claude models Tug uses extended thinking with a mapped token
budget. Tug renders returned thinking inline in the transcript, persists it in
session JSONL files, and replays it on resume so follow-up turns can resend the
same assistant reasoning blocks.
OpenAI Codex
Tug can talk to the ChatGPT Codex backend using saved OAuth credentials.
Example config:
type = "openai-codex"
model = "gpt-5.5"
[openai_codex]
base_url = "https://chatgpt.com/backend-api"
Then start Tug and run:
/login
Current behavior:
- Tug stores the Codex OAuth credential in
~/.config/tug/auth.jsonor$XDG_CONFIG_HOME/tug/auth.json - Tug refreshes the access token automatically when it expires
- Tug supports
/model set openai-codex:<id>for a built-in list of Codex models - Tug supports
/thinking off|low|medium|high|xhighandCtrl+XthenLfor selecting Codex reasoning effort, sent on each turn - Tug renders Codex reasoning inline, persists it in session transcripts, and reuses it on resumed turns
- the in-app login flow currently depends on the browser callback reaching
http://localhost:1455/auth/callback - manual paste fallback is not implemented in the TUI flow yet
Google Antigravity
Tug can talk to the Google Cloud Code Assist ("Antigravity") sandbox, which exposes Gemini 3, Claude, and GPT-OSS models through a Google account OAuth login.
Example config:
type = "google-antigravity"
model = "gemini-3-flash"
[google_antigravity]
base_url = "https://daily-cloudcode-pa.sandbox.googleapis.com"
Then start Tug and run:
/login antigravity
Current behavior:
- Tug stores the Antigravity OAuth credential (access token, refresh token, and Cloud Code Assist
projectId) in~/.config/tug/auth.jsonor$XDG_CONFIG_HOME/tug/auth.jsonunder thegoogle-antigravitykey - Tug refreshes the access token automatically when it expires
- Tug supports
/model set google-antigravity:<id>for a built-in list of models - Tug supports
/thinking off|low|medium|high|xhigh, mapped to Gemini 3thinkingLeveland to a token budget for other models - Tug renders Antigravity reasoning inline and round-trips tool calls across turns
- the in-app login flow depends on the browser callback reaching
http://localhost:51121/oauth-callback - on 403/404 the provider cascades across the daily → autopush → prod Cloud Code Assist endpoints
Model Switching
Tug supports live model switching through slash commands backed by the public models.dev catalog.
Available commands:
/model: list tool-capable models for the current provider catalog/model search <query>: search across supported catalogs/model set <provider>:<id>: switch the live session to that provider, endpoint, and model/model set <id>: same as above when the model id is unambiguous across supported catalogsCtrl+L: open the in-app model picker and switch without leaving the TUICtrl+XthenL: open the in-app thinking effort picker
Important behavior:
- model switching is session-only; it does not rewrite
tug.toml - Tug saves the last selected session target to
session.jsonin the Tug config directory - Tug may save the current session file there, but startup still begins with a fresh session
- Tug uses the provider
api,env, anddocmetadata frommodels.dev - switching providers also switches the live API base URL and API-key lookup rules
- you can override the endpoint for a specific
models.devselector with[models_dev.overrides."provider:model"] - you can also set a provider-wide override with
[models_dev.overrides.provider]or[models_dev.overrides."provider:"] - custom configured models remain separate from
models.dev; Tug only marks a model as current in the catalog when it was explicitly selected from the catalog - Tug caches the downloaded catalog in
~/.config/tug/models.jsonor$XDG_CONFIG_HOME/tug/models.json - on startup, if that file does not exist yet, Tug downloads it immediately
- the cache is reused for up to 12 hours and refreshed from the network after that
- if refresh fails and a cached catalog exists, Tug falls back to the cached catalog
Sessions
Tug now stores chat sessions as append-only JSONL files under the Tug config
directory, using the same broad session-file shape as pi-mono:
$XDG_CONFIG_HOME/tug/sessions/--<cwd>--/*.jsonl~/.config/tug/sessions/--<cwd>--/*.jsonl
Each session file starts with a session header and then appends message
entries for user/assistant turns plus Tug-specific custom entries for tool
rows, usage accounting, compaction checkpoints, and session target changes.
Available session controls:
/session resume: open the in-app session picker for the current workspace/resume: alias for/session resume/session new: start a fresh empty session with the current model/new: alias for/session new/compact: summarize older conversation into a reusable checkpoint immediately/thinking: show the current thinking level/thinking off|low|medium|high|xhigh: change the session thinking level/login: open the login provider picker (Codex or Antigravity)/login codex: start OpenAI Codex OAuth login directly/login antigravity: start Google Antigravity OAuth login directly/logout: clear saved OpenAI Codex OAuth credentials/logout antigravity: clear saved Google Antigravity OAuth credentialsCtrl+R: open the in-app session picker
Tug also supports Pi-style automatic compaction. When a completed turn reports context usage close to the model window, Tug summarizes older conversation into a checkpoint and future turns send that summary plus the recent messages. The defaults are:
reserve_tokens = 16384keep_recent_tokens = 20000context_windowis looked up from themodels.devcatalog at startup and after model switches; the hardcoded fallback is200000for Anthropic and128000for OpenAI and Codex
You can override those values in tug.toml:
[compaction]
enabled = true
reserve_tokens = 16384
keep_recent_tokens = 20000
# context_window = 128000
Tug also shows an inline slash-command picker directly from the composer when you
start typing /. Use Up / Down to move through matches, Tab to complete a
command into the composer, and Enter to complete or run an exact command.
On startup, Tug always begins with a fresh in-memory session. The JSONL file is
created only after the first user message is sent. Older sessions remain
available through /resume, /session resume, or Ctrl+R.
Supported catalogs today:
openaianthropicopenai-codexgoogle-antigravityzai-coding-planzaizhipuaiopencode-gocrof
API key lookup for /model set follows this order:
TUG_API_KEY- provider env vars from
models.devsuch asOPENAI_API_KEY,ANTHROPIC_API_KEY,ZHIPU_API_KEY,CROF_API_KEY, orOPENCODE_API_KEY - provider-specific fallback env vars already supported by Tug
- the current session API key, but only when the provider type stays the same
So if you switch from OpenAI to Anthropic, Tug will not silently reuse your OpenAI key.
Tools
Tug exposes the following tools to the model:
read: read a text file with optionaloffsetandlimitedit: exact text replacement in a single file, modeled after Pi’s edit contract (setedit_hashline = truein config to switch to the hashline line-anchored patch form)write: write a full file, creating it or replacing its contentsgrep: code search usingrgfind: file and directory name search usingfdbash: shell commands in the current workspacepython3: execute Python 3 code in the current workspaceterminal_start: start a long-running interactive terminal session in the background (tmux-backed)terminal_read: read recent output from a background terminal sessionterminal_send: send text or tmux-style keys (e.g.C-c,Enter) to a sessionterminal_stop: stop a background terminal sessionterminal_list: list active tool-managed terminal sessionstask: spawn a subagent with its own agent loop and context; returns an id to collect the result laterwait: block until one or more background subagents finish and return their results
Notes:
editis intentionally strict. Replacements must match exactly and must not overlap.edit_hashlinemode swaps the edit tool for hashline patches anchored by tags emitted fromread/grep; it is opt-in and off by default.grepdepends onrgbeing available on the system.finddepends onfdbeing available on the system.bashruns inside the current workspace and returns combined command output.terminal_*tools depend ontmuxbeing available on the system.python3depends onpython3(orpython) being available on the system.taskandwaitare subagent tools. Subagents run their own agent loop with isolated context and cannot spawn further subagents. Each subagent definition lives in a YAML-frontmattered markdown file and can specify a dedicated model and optional tool allowlist.
Keyboard Controls
Enter: send prompt (steers the running agent when a response is in progress)Alt+Enter: queue a follow-up prompt to send after the running agent finishesAlt+Up: pop the latest queued follow-up back into the composerShift+Enter: newlineCtrl+J: newline fallbackCtrl+G: open draft in$EDITOR!command: run a local shell command and add its output to future model context!!command: run a local shell command without adding its output to model contextCtrl+L: open the model pickerCtrl+R: open the session pickerTab: complete the selected inline slash commandEsc: clear queued steering first, then interrupt current model runCtrl+C: clear prompt, then quit on a second consecutive pressCtrl+U: clear promptLeft/Right/Up/Down: move inside the composerUp: recall the previous sent prompt when the cursor is already on the first draft lineDown: move toward newer recalled prompts, then restore the current draftHome/End: jump within the current linePgUp/PgDn: scroll the conversation
Caching
Tug surfaces cache usage in the footer when the provider returns token usage with cached prompt information.
Important distinction:
- For OpenAI, prompt caching is automatic on supported models.
- For generic OpenAI-compatible providers, caching behavior is provider-specific.
- Tug enables Anthropic request-side automatic caching for Anthropic models through
rig-core. - In
rig-core(currently 0.37.0), Anthropic caching is handled on the request path for Anthropic sessions; the OpenAI-compatible chat-completions path does not have the same explicit caching APIs.
So Tug can show cache hits and cache writes when the backend reports them, enables automatic caching for Anthropic sessions, and still does not force caching on for third-party OpenAI-compatible providers.
Architecture
- src/main.rs: terminal lifecycle, event loop, and command dispatch
- src/app.rs: app state, feed state, input handling, and
AgentEventprocessing - src/ui.rs: message feed and footer rendering
- src/agent.rs: model runtime, streamed tool events, and subagent spawning
- src/tools.rs: local tool implementations (
read,edit,write,grep,find,bash,python3,terminal_*,task,wait) - src/config.rs: config resolution and environment overrides
- src/prompt.rs: built-in system prompt,
AGENTS.mdloading, and skill attachment - src/session.rs: append-only JSONL session persistence and resume
- src/compaction.rs: automatic and manual context-window compaction
- src/models.rs:
models.devcatalog caching, model resolution, and context-window lookup - src/search.rs:
fff-searchindex backinggrepandfindtools - src/subagents.rs: subagent definitions, YAML frontmatter loading, and runtime manager
- src/codex.rs: OpenAI Codex provider (OAuth, SSE, WebSocket streaming)
- src/antigravity.rs: Google Antigravity provider (Google OAuth, Cloud Code Assist streaming)
- src/acp.rs: Agent Client Protocol (ACP) server implementation
Status
Active development. Most core features are in place. Areas still evolving:
- richer edit previews and approvals
- provider-specific caching knobs where available
- ACP server stability and feature coverage
- more subagent orchestration options