Optional extensions and a theme for Pi: a richer footer, usage-limit awareness for the agent, activity and timing indicators, background shell jobs, background subagents on the model of your choice, bounded shell execution, Kagi subscription search, local voice dictation, and clearer tool rows.
Requires Pi 1.2.0 or newer, Node 22.18 or newer, npm and Git.
pi-extras follows Pi's latest release; after updating pi-extras, update Pi too.
The shell-job extension currently targets macOS and Linux. A job runs in the
shell of Pi's bash tool, with its shellPath and shellCommandPrefix settings.
Windows process-group behavior has not been validated.
pi install git:github.com/jaedync/pi-extrasRestart Pi after installation. Use pi config to select extensions. Installing
adds all seventeen extensions (quota hibernation, computer use and Windows use stay off until you opt in); it makes quiet available but does not select it. It also adds the prompt templates /deeper, /higher and /wider.
Choose the theme using /settings. Use only one custom footer at a time.
Phase Spinner wraps an existing editor where possible; other editor extensions
can still conflict.
| Component | Behavior |
|---|---|
| Status Plus | Usage/cost grid, context and cache indicators, per-provider limits in a fixed order, $0.00 rows for free local providers, optional linked subagent usage |
| Usage Guard | usage tool, /usage command, one-shot wrap-up warnings for a session budget or, when enabled, near a limit |
| Cache Compaction | Prefix-sharing summaries that reuse the session's warm prompt cache, with safe fallback to Pi's default compaction |
| Rate-limit Recovery | /rate-limit-recovery, bounded backoff for short rate limits, opt-in main-session hibernation for provider cooldowns; subagents fail fast on quotas with reset guidance |
| Phase Spinner | Descriptive status with a per-mode spinner in the editor's top divider, with a per-step stopwatch, alongside tokens/sec, time to first token and total elapsed time; live thinking above queued messages and a π end line when a prompt finishes |
| Tab Status | Pi's state in the terminal tab: an iTerm2 status dot and detail, and tab progress in iTerm2, Ghostty, WezTerm and Windows Terminal |
| Shell Jobs | shell_job_start, shell_job, /jobs, bounded logs and completion notifications; jobs are named after their titles |
| Subagents | subagent, message and stop_subagent tools, /subagents: background child agents on your scoped models that report back, talk to main and to each other, each with a live row above the editor |
| Bash Default Timeout | Adds a 120-second timeout only when a bash or powershell call omitted one |
| Kagi Search | Adds kagi_search without replacing existing search/fetch tools |
| Voice | Hold or tap ctrl+space to dictate into the editor, transcribed on this machine |
| Computer Use | Opt-in, macOS: a computer_use tool that operates Mac apps through OpenAI's Computer Use, installed by the ChatGPT app |
| Windows Use | Opt-in, WSL on a Hyper-V host: a windows_use tool that operates Windows VMs through Windows-MCP, which it installs in each guest, and through their consoles |
| Tool Display | Every tool row as a colored header band with live progress and a popup with the whole call, other extensions' tools included; chained bash commands broken into steps; every finished row stays visible, ctrl+o expands them all |
| Pull Link | pull_link tool: give it a link to read posts with their threads, replies and images (X, Bluesky, Threads, Mastodon, Reddit, Hacker News, LinkedIn), GitHub pages, images, any web page, and videos with transcript, thumbnail, comments and frames on request (YouTube, TikTok, Instagram, Vimeo and more); the links in your messages are read before the model starts (/autopull) |
| Copy Blocks | Code blocks and quotes in replies drawn on a background of their own with a copy label: one click copies the exact text; /copy-block does it from the keyboard |
| Release Notes | What changed in pi-extras, shown one time in the first new session after an update; /pi-extras changelog shows it again |
| Quiet | Low-contrast theme with restrained accent colors |
| Prompts | /deeper, /higher and /wider [task]: before work starts, the model plans a deeper version of the task (expert methods, raw material, an outside check), the outcome behind the task, or the other fields that can improve it. Then it shows a new prompt and waits for your approval. From Felix Rieseberg's How to be more ambitious |
pi update --extensions
# Or update Pi and packages together:
pi update --all
pi remove git:github.com/jaedync/pi-extrasAn unpinned Git install follows upstream commits. Pi can detect available package
updates. This package does not update itself and adds no background service.
Only the Pull Link media tools update themselves (see
Video tools); PI_OFFLINE=1 stops that. The Pull Link media
tools install on the first video link. Voice installs when Pi finds a
microphone. To pin a published tag, append it to the Git source, for example
pi install git:github.com/jaedync/pi-extras@v0.24.14.
Pinned installs require an explicit installation of a newer tag.
After an update, restart Pi or use /reload; stop active jobs before removing
the package. Removing it does not remove your credentials or change other packages.
STATUSLINE_TZ: optional IANA timezone for footer clock/reset labels; defaults to the system timezone. An unknown name also gives the system timezone, and Status Plus writes a note instatus-plus.log. If the system timezone is not known (for example, withTZ=""), times use UTC.STATUS_PLUS_POLL_LIMITS=0: disable authenticated quota polling while retaining recorded usage and response-header limits.PI_OFFLINEalso suppresses polling. As in Pi, onlyPI_OFFLINE=1,trueoryesturns offline mode on. Pi processes that share an agent directory poll each provider one time between them. They share the result inPI_CODING_AGENT_DIR/status-plus-limits/, so a new session shows the last budget immediately.PI_EXTRAS_USAGE_GUARD=1or0: turn band warnings on or off for one run. See below for the persistent setting.PI_CODING_AGENT_DIR/pi-extras.json(default~/.pi/agent/pi-extras.json): persistent Usage Guard settings underusageGuard, written by/usage warnings on|off. Keys:enabled(defaultfalse),bands(default[90, 95]),resumeMarginSeconds(default180),proximityPct(default10),maxWaitSeconds(default18000, five hours).PI_RATE_LIMIT_RECOVERY=on|off: override automatic quota waiting for one run.rateLimitRecoveryinpi-extras.json:autoWait(defaultfalse),resumeMarginSeconds(default10,0to3600),maxWaitSeconds(default and hard maximum18000, five hours total per user-started run),maxRecoveries(default and maximum10),anthropicFirstEventSeconds(default45,10to600,0disables the Anthropic stall retry),transientMaxWaitSeconds(default180,10to900,0leaves short rate limits to Pi's own retry). A number out of range moves to the nearest limit. A negative number, or a value that is not a number, uses the default. A count with a fraction rounds down./rate-limit-recovery on|offsaves onlyautoWait; commands take precedence for the current session.PI_RATE_LIMIT_RECOVERY_ROLE=subagentmarks external child sessions as detection-only.PI_SUBAGENTS=off: disable Subagents.subagentsinpi-extras.json, all optional:defaultModel(a model or short name; default: the session's model),maxConcurrent(default4),batchMs(default2000),groupWaitMs(default60000),replyTimeoutMs(default600000),maxRunMinutes(default60, at most1440) andmaxRunCost(US dollars, defaultnullfor no limit), the budget of each run of a child,childToolsExclude(tool names children never get) andresumePolicy("reload", the default, resumes children that a/reloadinterrupts;"always"also allows one automatic attempt after a restart or crash;"notify"never resumes on its own), andfallbackModels(models, in order, that a run moves to when its model can't serve it; default: the default model;[]turns fallback off), andswapAtTurn(defaulttrue;falsekeeps a working child on its session until its run ends, also when that session is not current). The model guide isPI_CODING_AGENT_DIR/subagent-models.md, plus.pi/subagent-models.mdin a project that main trusts. See below.PI_BASH_DEFAULT_TIMEOUT: seconds;0oroffdisables the injected timeout. A value above 2147483.647 (the longest timer Pi can set) falls back to 120. Explicit per-call timeouts are preserved. Subagent children apply the same default to theirbashandpowershellcalls, also when a package filter turns off the Bash Default Timeout extension for main. Main and its children get a rule in the system prompt that gives the default, because Pi's ownbashschema says that the timeout has no default. The rule is only in Pi's default system prompt. ASYSTEM.mdreplaces the part that holds it.PI_CACHE_RETENTION=long: use the hour-long cache-warmth window when the transcript doesn't say. Anthropic replies report hour-long cache writes and Status Plus follows them, so a proxy that always writes them, such as Meridian, needs no setting.PHASE_SPINNER_DEBUG=1: opt-in local timing diagnostics. Leave off normally.PI_VOICE=off: disable voice dictation.PI_VOICE_KEY: a different key (defaultctrl+space).PI_VOICE_HOME: where voice keeps its runtime, models and settings (default~/.cache/pi-extras/voice, or underXDG_CACHE_HOME).PI_COMPUTER_USE=on: enable computer use on macOS. Off by default. See below.PI_WINDOWS_USE=on: enable Windows use in WSL on a Hyper-V host. Off by default. See below.PI_WINDOWS_USE_VMS: the only VMs it may use, comma-separated (default: all).PI_TOOL_DISPLAY=off: leave Pi's own tool rows in place./tool-displaywrites its switches undertoolDisplayinpi-extras.json:enabled,others(defaulttrue),chains(defaulttrue),motion(fullorreduced), andthinking(tail,collapsedorfull; defaulttail).phaseSpinner.verbsinpi-extras.json: opt into playful spinner words with"playful"(the built-in pie and π list), or up to 100"Present|Past"pairs such as["Simmering|Simmered"]. Without this setting, the status names the phase descriptively.PI_COPY_BLOCKS=off: leave code blocks and quotes in replies as Pi draws them.statusPlus.toolCountinpi-extras.json:steps(the default) orcalls, switched by clicking the footer's tool count or with/tool-display count.statusPlus.copyLinksinpi-extras.json:falsestops the link copy in a remote session (defaulttrue). See Status Plus.releaseNotes.seeninpi-extras.json: the last pi-extras version whose notes were shown.KAGI_TOKEN_FILE: path to your own subscription session credential. See below.KAGI_TOOL_NAME=web_search: explicit search-tool replacement. Leave unset to keepkagi_searchand avoid conflicting with another search extension.
See security and privacy before enabling provider-limit polling or supplying search credentials. Extensions execute with your user permissions.
How pi-extras.json is saved. Each extension keeps its settings in its own
section. A save reads the file again and changes only that section. A short
lock, pi-extras.json.lock, makes Pi sessions that save at the same time wait
for each other. A save waits for the lock for up to 3 seconds, then fails with
a message. The new text goes to a temporary file, which then replaces the old
file. When pi-extras.json is a symbolic link, the save writes the file that
the link points to and keeps the link. The lock and the temporary file are in
the folder of that real file, for example your dotfiles folder. The file keeps
its permissions, and a new file gets mode 0600.
When the file does not parse (for example, after an edit that leaves an extra comma), pi-extras does not write it. Each extension uses its defaults, and each save shows an error that names the file. A new session in the terminal also shows a warning. Correct or remove the file to save settings again.
The Status Plus context figure (33k / 100k and its bar) shows the context
size against the point where Pi compacts the session. That point is the
window less compaction.reserveTokens, or less the reserve in
compaction.modelOverrides for the selected model. Thus 100% means Pi compacts
now, and the figure can go past 100% until it does. With compaction off, or
with a reserve as large as the window, the figure uses the whole window. It
turns amber above 70% and red above 90%. After a compaction the size is ?
until the next reply. The figure reads the settings files again when one of
them changes, and shows the change at the next entry in the session. Pi uses
a changed file only after /reload. Thus, after you edit a settings file, use
/reload so that Pi and the figure agree.
A click on a status line. Status Plus shows the status of each other
extension (ctx.ui.setStatus(key, text)) on a line of its own below the grid.
In fullscreen mode, a left click on one of these lines sends an event on Pi's
event bus. Other extensions can use this event as a contract:
- Name:
status-plus:click. - Payload:
{ key }.keyis the key that the extension gave toctx.ui.setStatus. - When: the left button goes down and up at the same point on the footer line of that status. Today Status Plus cuts a long status to one line. If a later version wraps a status, each of its lines sends the same key.
- Only in fullscreen mode. In regular mode the terminal keeps the mouse, so the event does not occur.
- Not for the remote-pi statuses, because the grid shows them in one cell. A click on the tool count changes the count and sends no event. A click on a link in a status line goes to the link.
Status Plus does nothing else with the click. The extension that set the status decides what occurs:
pi.events.on("status-plus:click", (data) => {
if ((data as { key?: unknown } | undefined)?.key === "my-extension") showDetails();
});Links in a remote session. In fullscreen mode, Pi catches the mouse. When
you click a link, Pi opens it with xdg-open (Linux) or open (macOS) on the
machine that runs Pi. Over SSH, that is the remote machine. On a remote machine
without a desktop, the click does nothing and shows no error. Thus, in a remote
session, Status Plus copies the link and shows Copied link: <url>.
- A session is remote when
SSH_CONNECTION,SSH_CLIENTorMOSH_CONNECTIONis set, or whenPI_CLIPBOARD_OSC52=1. This is the same test that Pi uses for its clipboard. - The copy uses Pi's clipboard function. In a remote session it sends the text through the terminal (OSC 52) to the clipboard of the computer in front of you. Your terminal must accept OSC 52. On the maintainer's Mac the link gets to the clipboard. On the maintainer's Windows PC it does not get there today. When the link is not in your clipboard, select it in the notice.
- When the copy fails, a warning shows the link, so that you can select it.
- A local session opens links as Pi does.
- To turn this off, set
statusPlus.copyLinkstofalseinpi-extras.json, then use/reload.
Tab Status shows Pi's state in the terminal tab.
- iTerm2 3.7.0+ gets a colored Session Status dot with a short detail:
accent while working, warning while a dialog waits for you, dim when idle,
and red after a failed turn until the next prompt. Working details name the
current phase or tools. Idle details say
Done; settabStatus.detail: "reply"to show the first 80 characters of the last reply instead. - Progress animates the tab while Pi works, pauses during dialogs and Rate-limit Recovery waits, turns red after a failed turn, and clears at idle. In Windows Terminal, a full yellow ring means Pi is waiting, either for your input in a dialog or for a rate-limit wait to finish, and a full red ring means the last turn failed.
Background Subagents and Shell Jobs count as working by default, even after main finishes.
Progress turns on by itself only in terminals known to support it, because older terminals can show every progress update as a notification:
| Terminal | Automatic from | Notes |
|---|---|---|
| iTerm2 | 3.6.7 | 3.6.6 supports progress but doesn't advertise it; set progress: true |
| Ghostty | 1.2.0 | |
| WezTerm | nightly 20250209-182623-44866cc1 |
No paused state, so waits stay indeterminate |
| Windows Terminal | 1.6 | It doesn't report its version, so WT_SESSION alone turns it on; older versions ignore it |
Unknown terminals and older versions stay off. Windows Terminal is the
exception: WT_SESSION turns progress on, in WSL tabs too, since Windows
Terminal passes it into WSL. SSH sessions don't get WT_SESSION, so they stay
off. Another terminal's TERM_PROGRAM, such as vscode, wins over an
inherited WT_SESSION. iTerm2 is detected from TERM_PROGRAM=iTerm.app, or
from LC_TERMINAL=iTerm2 (which also works over SSH) when no other terminal
sets TERM_PROGRAM. Inside tmux or screen, the version comes from
LC_TERMINAL_VERSION, never from the multiplexer's own version.
If Pi's terminal.showTerminalProgress setting is on, Pi owns progress
instead; Pi's bar is indeterminate only and ignores background work, pauses
and errors.
Inside tmux, add set -g allow-passthrough all so hidden panes can update the
tab. Tab Status wraps its sequences for tmux, resends busy status every second
and sends the final idle twice.
Settings in pi-extras.json, applied on /reload:
{
"tabStatus": {
"enabled": true,
"sessionStatus": "auto",
"progress": "auto",
"busyWhileBackground": true,
"detail": "done"
}
}sessionStatus and progress take "auto", true to skip the version check,
or false to turn them off. Session Status still needs iTerm2. Force progress
only in a terminal you know supports it.
- Only the top-level interactive session writes to the terminal. Print, JSON and RPC modes and subagents stay silent.
- Both fields reset on startup and clear on exit.
- Idle waits 1.5 seconds, so short gaps between turns don't set off completion alerts.
- A reload keeps the tab as it is until the new runtime knows the background counts. Turning Tab Status off and reloading clears only what it set.
- A crash, or uninstalling pi-extras and then reloading, can leave a stale tab until another program resets it.
iTerm2 can show these details in its Cockpit and in status-change alerts; see security and privacy.
On by default. To compact, Pi normally sends the conversation to the model as a new summarization prompt. That prompt can't use the provider's prompt cache, so the whole context is paid for again at full price. Cache Compaction sends the summary request as the session's next turn instead: the same system prompt, tools, history, reasoning settings and session ID, with a summarization instruction added at the end. The provider reads almost all of it from cache.
Measured on this machine (provider-reported costs, not invoices):
| Context | Model | Pi's compaction | Cache Compaction |
|---|---|---|---|
| ~15k | Claude Sonnet via Meridian | $0.048 | $0.015 |
| ~15k | Codex | $0.019 | $0.0066 |
| ~42k | Claude Sonnet via Meridian | $0.169 | $0.010 |
| ~42k | Codex | $0.055 | $0.0034 |
In live automatic compactions at a 48k window, about 32k of the 33k input tokens came from cache, on both Claude and Codex. The summaries were comparable to Pi's own in a two-model comparison. Results vary by provider and workload.
When it runs. In the main session and in pi-extras subagents, for
/compact and for Pi's automatic compaction after a turn or before a new
prompt, as long as:
- the cache is probably still warm (see
idleSecondsbelow); - the model, session and branch haven't changed since the last request;
- the summary fits in the context window (see below).
Otherwise Pi's own compaction runs, exactly as it would without this extension. The notice says which path ran and why. The summary request streams, so the Phase Spinner can show its newest lines while it is written. The request is the same as one sent without streaming.
Supported APIs. Anthropic Messages (anthropic-messages), OpenAI
Responses (openai-responses, openai-codex-responses,
azure-openai-responses), Google (google-generative-ai, google-vertex)
and OpenAI chat completions (openai-completions). Local engines such as
llama.cpp, vLLM and other OpenAI-compatible servers use chat completions. For
other APIs, Pi's own compaction runs with the reason unsupported-api.
Local engines. Many local engines keep a prompt prefix in memory until
they need the space. For openai-completions models with a local baseUrl,
the default idle limit is 25 minutes. A local baseUrl is a loopback, private
or Tailscale (100.64.0.0/10) address, 0.0.0.0, such an address in IPv6
(::ffff:192.168.1.5), a host name without a dot, or a name that ends in
.local, .lan, .internal, .localdomain, .home.arpa or .ts.net. This
stays under a typical gateway idle window. When the cache is cold, the summary
request is a full prefill of the conversation, which is larger than Pi's own
summary prompt. Set idleSeconds for the provider if your engine evicts
sooner, or if the local baseUrl is a proxy to a cloud API.
Some local engines do use a timer. Ollama unloads an idle model after
OLLAMA_KEEP_ALIVE (5 minutes by default), and LM Studio can unload a model
after an idle TTL. The cache goes with the model. For these engines, set
idleSeconds for the provider to a value below that timer, for example
"ollama": 240.
Measured on a local OpenAI-compatible engine (Qwen3.8-Flash-Next, one synthetic session of 18k tokens with two tool calls): the summary request had 19,229 prompt tokens, and 18,311 of them came from the cache. Only the 918-token instruction was new, and the first token came after 1.2 s.
Chat templates. A chat-completions engine renders the messages with the
model's chat template. The summary request keeps every field that changes the
rendered prompt: the messages, tools, tool_choice, reasoning_effort and
the template arguments. Only the output limit (max_tokens or
max_completion_tokens, as the request used) changes, and the instruction is
a new user message at the end. The extension does not set
tool_choice: "none", because some engines then render the prompt without
the tool list, and the cache misses from the first system token. If the
model calls a tool in the summary turn, the tool does not run, and Pi's own
compaction runs with the reason unusable-summary.
Some templates, such as recent Qwen3 templates, keep the reasoning of earlier
turns by default. With them, the new user message does not change how earlier
turns render. If a client sends
chat_template_kwargs: { "preserve_thinking": false }, the template removes
the reasoning of all turns before the last user message. Then the cache
covers the conversation only up to the first assistant message after the
previous user message (36% of the prompt in a synthetic check).
Subagents always load Cache Compaction, even when a package filter leaves it
out of the main session. cacheCompaction.enabled: false turns it off in both.
Log. Each decision adds one JSON line to
PI_CODING_AGENT_DIR/cache-compaction.log (default
~/.pi/agent/cache-compaction.log): the session ID, provider and model, why
Pi compacted, which path ran, the fallback reason and the token estimates.
Cache-path attempts also record the stop reason and token usage. It never
holds messages, summaries, headers or payloads. With Tool Display loaded, the
session's compaction entry also saves the path and fallback reason, outside
the model's context. Fallback reasons for a missed capture start with
capture-: no-system, no-model, projection, identity, payload,
no-context, headers and failed.
Room for the summary. The summary is written inside the same context window as the conversation, so it needs room. The extension asks for room for the previous summary plus 8,000 tokens (6,000 for new material, 2,000 for reasoning), and keeps a safety margin of 4,096 tokens plus 60% of whatever was added since the last request, because that part is only estimated.
At Pi's default compaction.reserveTokens of 16,384, an automatic compaction
has about 12k tokens of room before anything added since the last request.
First summaries fit. Later summaries in long sessions often don't, because
each one carries the previous summary forward. Out of 183 compactions in local
sessions, 58% would have had room at the default, and all of them at 32,768 or
49,152. (Method: the model-written part of each saved summary, at four
characters per token, against room for the previous summary plus 8,000.) On
large-window models, raise the reserve in Pi's settings.json; 32k is about
3% of a 1M-token window:
{ "compaction": { "reserveTokens": 32768 } }With a 258,048-token window and a reserveTokens of 24,576, an automatic
compaction at the trigger has about 18k tokens of room. A first summary fits.
An update fits while the previous summary is below about 10k tokens. Each
token that the context goes past the trigger removes one token of room. When
the room is too small, Pi's own compaction runs with the reason
context-window.
Settings. In PI_CODING_AGENT_DIR/pi-extras.json (default
~/.pi/agent/pi-extras.json), then /reload:
{
"cacheCompaction": {
"enabled": true,
"idleSeconds": { "anthropic": 3300, "openai-codex": 240 }
}
}enabled:falseleaves Pi's compaction untouched.idleSeconds: per provider, how long after the last request the cache counts as warm, from 0 to 86400 (0 means never use the cache path). The default is 55 minutes for Anthropic through Meridian on port 3456, which writes one-hour caches, 25 minutes for chat completions on a local engine (see above), and 4 minutes for everything else. The example'santhropicvalue also applies to Anthropic's own API, which keeps caches for 5 minutes, so leave it out unless every Anthropic route you use keeps long caches.
Providers. Anthropic Messages, OpenAI Responses (including Codex and
Azure), Google Generative AI and Vertex, and OpenAI chat completions. Other
APIs use Pi's compaction.
Tested with Pi 1.0.0. If another extension changes requests in
context_with_system, before_provider_headers or before_provider_request,
load it before this one.
Also falls back when: Pi is recovering from a context overflow; the place where Pi's kept messages begin can't be identified unambiguously; Pi is blocking images in the session; or the reply is an error, empty, a tool call, or cut off at the output limit. Codex requests don't declare an output limit, so for Codex the room check is the extension's own guard rather than a limit the provider enforces.
Cost of a failed attempt. If the cached request fails, the provider still bills it, but it isn't counted in the session's totals. Pi's compaction then runs as usual. See security and privacy for what is kept in memory.
Usage Guard reads the limit snapshots Status Plus polls. It never polls limits
on its own, except for a usage call with refresh: true or all: true.
all: true polls each scoped model's provider that has no snapshot yet. For
dollar figures, a usage call or /usage also reads local session files and,
when set up, Tokenfold and OpenRouter (see Dollar figures).
PI_OFFLINE stops the Tokenfold and OpenRouter requests. Only windows that
govern the active model count: provider-wide windows always, model-specific ones
(such as an Anthropic seven_day_fable bucket) only when the active model id
carries that family. Balances (Enterprise spend, prepaid credits) and per-minute
rate limits taken from response headers are reported but never warned on.
usagetool: percent used, thresholds, reset time, seconds until reset andresumeAfterSeconds(reset plus margin) per window, andwaitablewhen the reset is exact and withinmaxWaitSeconds.setBudgetrecords a session budget ("work until 60% of the weekly limit");allincludes other providers and non-governing windows.- Known window durations and exact reset times add a
paceobject to each limit. It gives the elapsed fraction, expected usage at even pace, and projected usage at reset. A projection from 90% through 110% ison pace. This band allows small request bursts and percentage rounding. Higher projections areabove pace; lower projections arebelow pace. Above-pace limits also give the estimated time to 100%, in ISO and local time. Before 5% of the window passes, or below 1% used, the state istoo early to tell. These early limits omit the projection and the time estimate. The tool rows, popup, and warning text show pace. Pace does not change warning thresholds. Estimates use the average rate since the window began, not the last request. - Warnings are off by default: only a session budget warns, one time, when its
window is reached.
/usage warnings onadds band warnings (90 and 95 by default) and provider blocks. Each fires one time per window, threshold and reset cycle, immediately before a model request. It is a message appended to context, without changing the system prompt or initiating another request. Idle polls do not queue warnings for later delivery: the active provider and model family are checked again for manual prompts, automated wakeups and queued follow-ups. Obsolete automatic warnings remain in raw history. They are omitted from requests for models they do not govern, and from every request after their window has reset. Resets reported within ten minutes of each other count as one cycle, since proxies recompute them on every fetch. The first band is advance notice only. The final message never cuts short work that fits: for a waitable reset (any window, weekly included, withinmaxWaitSecondsof its reset) it tells the agent to finish what fits, then sleep through the reset in a background job (sleep resumeAfterSeconds) and carry on, so the run outlasts the limit instead of halting. A reset days away or a session budget asks for a clean checkpoint and a summary. A model-scoped window notes that other models are unaffected. Fired keys and the budget persist with the session, so a resumed session does not repeat them. - Proxy rejection flags can remain after a quota reset. A later reset cycle or a successful response from the applicable model clears that flag below 95% used. A new structured quota error restores the block. A rejection without newer evidence stays blocked. Cleared block warnings remain in history but do not enter later model requests.
- A proxy such as Meridian can answer the quota poll without its usage-endpoint
source. Then it sends only Claude Code's last readings, which can lag or read high.
- Such a reading does not replace a usage-endpoint reading that is less than 30 minutes old. A window that the answer does not include keeps its last reading until its reset.
- A Claude Code reading that is more than 15 minutes old is ignored. A rejection with a future reset still blocks the window. A rejection with no percent blocks the window and shows as an approximate 100%.
- A Claude Code reading is approximate. A kept usage-endpoint reading that is more than 30 minutes old is also approximate.
- The footer shows
~before the percent of an approximate reading, for example7d ~96%. Theusagetool marks it withapproximate. - An approximate reading fires no band or budget warning, has no dollar figures, and does not go into the granted-reset readings.
- A Claude Code reading can differ by 20 points or more from the last
usage-endpoint reading of its window, more than normal use explains. Then
each Pi process that polls writes one line with both readings to
status-plus.log. The line gives the percentages, reset times, ages and the proxy's status and warning threshold.
/usagequeues the current snapshot for the next turn, or steers it into the run while the agent works;/usage budget 7d 60and/usage budget clearmanage the session budget;/usage warnings on|offpersists the toggle.- Polling: providers whose window sits within
proximityPctof a threshold poll at their faster cadence; failed polls back off exponentially up to ten minutes. The Pi processes of one agent directory share one poll and one backoff, and a provider'sRetry-Afteris obeyed for up to an hour.
The usage tool also gives money, so that an agent can budget work and
subagents in dollars. Each limit gets a dollars object, and the report gets
a top-level dollars summary.
- Spend today: the summary gives the spend on this machine since local midnight, by provider. With Tokenfold, it also gives the spend on all personal machines. Spend is the API list price of the tokens, from the model catalog. It is not a bill.
- Subscription windows (Claude 5h and 7d, Codex): a provider reports only
a percent. The tool divides the spend in the window by the percent used to
get the window size in dollars (
basis: "implied"). Then it gives the dollars that remain. Below 5% used, the tool uses the last size it saw for that window in the last 8 days (sizeFrom: "earlier window"). Without one, it gives only the spend. The figure changes with the model mix, so use it as an estimate. - Tokenfold: when
usageDollars.tokenfoldis set, Claude windows use Tokenfold's spend for all machines on the account (source: "tokenfold"). Without it, the tool counts the spend on this machine only. Then use on other machines makes the limit read low, which is the safe direction. A Tokenfold window with a different reset is ignored. The tool matches a Tokenfold window to this account by its reset time only, so set Tokenfold only on machines that use the account Tokenfold tracks. - Granted resets: a provider can reset a window before its reset time.
The usage before that reset does not count, so the spend in the window
starts at the reset. Tokenfold gives this start (
window_start).- The tool also finds granted resets. It keeps a reading of each window at each limit poll. Polls occur only while Pi runs.
- A fall of 10 points or more, or a fall to 0%, with the same reset time is a granted reset. If the next reading is at 80% or more of the earlier level, the tool cancels the reset.
- The spend starts at the latest of these times: the cycle start, the Tokenfold start, and a granted reset that the tool found.
- Sometimes the tool finds a granted reset that is later than the Tokenfold start. Then the tool uses the spend on this machine from the reset. A note tells you this.
- A model-family window uses the Tokenfold start only when its own percent also fell at that time. Otherwise it uses only the resets in its own readings.
- Sometimes a reset comes after a gap of more than 20 minutes in the
readings. If the Tokenfold start is in that gap, the tool uses the
Tokenfold start. If not, the tool does not count the spend in the gap
(
spendStartApprox), and it does not keep that size for the next cycle.
- Model-family windows (such as
7d-fable): the dollars count only that family's spend and carrymodelFamily. They set a provider's summary only when they govern the active model. - OpenCode Go: the caps are dollars per model, from the OpenCode Go docs
(read 2026-10-10). The 5h window allows 20% of a model's monthly cap and
the weekly window 50%. The tool gives the remaining dollars per model
(
perModel) for the scoped models, the active model, and the models used in the last 7 days. Withall: true, the tool polls a scoped model's provider that this session has not used yet. The usage endpoint does not report the plan, so set it. - Monthly meters (Claude Enterprise, extra usage): the provider's own
spend and limit, the calendar days and business days (Monday to Friday,
local time zone) until the reset, and
perBusinessDayUsd. That share includes today's spend, so it does not change during the day.spentTodayUsdis the meter's growth since the first reading of the local day, andleftTodayUsdis today's share minus that spend. A negative value means today is over its share. With no reset from the provider, the next UTC month start is used (resetApprox). - OpenRouter: the credit balance, the key's spend for the current UTC day, the key limit that remains, and the budget that blocks first.
- Tool row: the band adds the dollars left for the active provider. The
popup shows each limit's dollars under its row, names the provider when
there is more than one, and ends with the summary. Press
jin the popup to copy the JSON that the model received. - Summary:
dollars.providers.<provider>givesspentTodayUsd, andremainingUsdwithbindingWindowfor the tightest window. For OpenCode Go, it givesremainingUsdByModel.unsizedWindowsnames the windows with no dollar size yet;remainingUsddoes not cover them, so plan less spend.
Settings, in ~/.pi/agent/pi-extras.json:
{
"usageDollars": {
"tokenfold": { "url": "https://usage.example.com", "keyFile": "~/.config/tokenfold-read-key" },
"opencodeGo": { "plan": "go", "monthlyCaps": { "glm-5.3": 15 } }
}
}tokenfold.urlmust usehttps, orhttpto this machine only. The key file holds a read-only key that Tokenfold accepts onGET /api/ha. The tool never shows the key or the key file path.opencodeGo.planisgo(the default) orgo-plus.monthlyCapsreplaces the cap of a model when OpenCode changes it.- Remote answers are kept for one minute.
refresh: truereads them again. When a source fails, the report gives a note and the other figures.
Usage Guard helps the agent checkpoint before a quota runs out. Rate-limit
Recovery handles the actual rejection without asking an unavailable model to
schedule a sleep. Detection is always on; /rate-limit-recovery on opts an
interactive main session into automatic waiting. /rate-limit-recovery status
shows the policy, off disables it, and cancel stops an active wait.
A structured rate_limit_error or rate_limit_exceeded with a numeric
retry_after in seconds supplies the reset estimate. The extension waits that
remaining delay plus its safety margin, shows a countdown, then makes one
continuation. Escape or Ctrl+C cancels; outside a wait, Pi keeps its usual key
behavior. Switching between Anthropic models, including compatible aliases,
retains the cooldown and uses the newly selected model on resume. Switching
outside that scope cancels. Other providers retain waits across models on the
same provider.
Before the retried request, a persisted message records the actual elapsed wall time and UTC pause/resume timestamps, plus the limited and current models. If the system clock moves backward, the notice labels a lower bound from the completed timer instead of claiming an exact wall-clock duration. Steering queued while waiting reaches the recovery request; explicitly deferred follow-ups remain deferred until the original task finishes, as in Pi. It is a timing notice, not proof that quota has reset. Each user-started run has a shared five-hour maximum wait budget (margins included) and at most ten recoveries by default. Excessive delays are refused, never shortened to retry early. Reload, session replacement and exit cancel; a restarted session never silently resumes a previous wait.
Anthropic subscription requests (OAuth, direct to api.anthropic.com) can be
held near :00 and :30 UTC: the response starts, then sends only keep-alive
pings for minutes, and Pi never retries because pings count as activity. If no
real event follows the headers within anthropicFirstEventSeconds (45 s), the
request fails as a timeout and Pi's normal auto-retry sends it again. API-key
requests, proxies such as a local gateway, and other providers are untouched.
Short rate limits get their own backoff, on by default in every session,
subagents included: OpenRouter's "temporarily rate-limited upstream" and other
429 or rate-limit errors without a structured reset. Pi's own retry gives up
after about 14 seconds, while these usually clear within a minute. The extension
waits 5, 10, 20, 40, then 60 seconds (each ±20% so parallel subagents spread
out, and never less than the 429's Retry-After), for up to
transientMaxWaitSeconds (180 s) per streak, then sends the request again. A
countdown shows in interactive sessions; Esc cancels, and switching models
resumes immediately with the new model. If the limit outlasts the budget, the run
ends with guidance. Quota, billing and usage-limit errors are not treated as
short limits. A limit reported inside an already started stream is recognized
only by its message text, because Pi keeps no status code or structured reset
for it: OpenRouter's "rate-limited upstream" wording gets this backoff, while a
generic message such as "Provider returned error" stays with Pi's own retry.
Meridian, a local Claude Code proxy, sometimes stops a turn with "Reached maximum number of turns". Pi does not retry this error. The extension sends the request again after 2 s, 5 s, then 10 s, in all sessions, subagents included. It does this only when no tool of the failed reply ran. After 3 retries, the run stops with an error.
Recognized quota errors remain errors and are classified as non-transient so
Pi does not run a competing short retry loop. This recovery policy is independent
of Pi's retry.enabled; ordinary transient failures retain Pi's native retries.
For native Anthropic and OpenAI HTTP adapters (including Azure and Codex), a
request-local guard also stops configured transport retries on recognized JSON
429 errors. It preserves endpoint, authentication and model configuration;
ordinary HTTP failures keep both transport and session retry behavior. Inspection
is bounded to 32 KiB and 500 ms. A legacy custom provider's guard stays on one
API; switching that provider to another API preserves its custom routing and
adapter retries rather than retargeting it. Built-in providers with several APIs
(OpenRouter, Copilot) move their guard to the selected API at the next request,
unless a subagent shares it. Unsupported transports, mixed-API
legacy requests and unrecognized formats retain adapter behavior; detection
runs when Pi reports the final error. Header-only reset timing is not guessed.
If waiting is off, timing is missing, or the budget is exhausted, the session
fails with reset guidance instead. Provider reset times are estimates.
Children never hibernate. The pi-extras child launcher installs an explicit quota guard that never sleeps, even when the parent has opted in: one quota rejection fails the child, frees its slot, and tells the parent the provider, expected reset time and time remaining. Hidden transport retries are suppressed on the guarded native HTTP paths described above; custom or unsupported adapter retries remain owned by those adapters. Print, JSON and RPC sessions likewise never auto-wait, so external noninteractive children cannot inherit a long sleep. The bounded short-limit backoff above still applies to them. External launchers that disable extensions must explicitly load detection; this package cannot intercept requests made outside Pi's extension runtime. The extension makes no quota polls of its own and pauses cache warming for the limited model scope until the estimated reset.
Hold ctrl+space and speak, or tap it to start and tap again to stop. Esc discards the recording, including after you stop while it is still being transcribed. The transcript is typed into the editor and never sent on its own; review it and press Enter yourself. Speech is transcribed in chunks at each pause while you talk, so when you stop only the last chunk is left to decode and the text starts typing in right away.
A recording row in the editor border shows a red dot, the elapsed time, a level meter, one mark per chunk, the microphone and the model. It uses the top border when that is free and the bottom border while the agent is working. Warnings there cover a mic that hears nothing, audio that clips, and a missing permission. After you stop, a wait longer than a second says what it is waiting for (starting voice, loading the speech model, or transcribing).
/voice opens a menu with the current mic and model. /voice mic picks the
input device (saved by name; a missing device falls back to the system default
with a one-time warning). /voice model picks the speech model, /voice status
shows what is installed and why a model was skipped, /voice setup retries
installation, and /voice unload frees the model's memory.
Platforms. macOS, and Linux with PulseAudio or ALSA (including WSLg). Linux has had less testing than macOS. Windows is not supported. Hosts without a microphone download nothing until you start a dictation.
First run. On a machine with a microphone, Pi installs voice in the
background: a private uv,
Python 3.12, the sherpa-onnx speech runtime, and NVIDIA Parakeet models. The
108 MB English model comes first; on machines with 8 GB of RAM or more the
487 MB multilingual Parakeet v3 follows and becomes the default. On Apple
Silicon, /voice model also offers an MLX build of Parakeet v3 (about 2.5 GB).
A model is skipped when the disk has less than twice its size free. Setup
never blocks Pi; a dictation started before it finishes waits for it. If setup
did not start with the session, the first dictation starts it, except offline.
On Linux, Pi first looks for an ALSA capture stream in /proc/asound/pcm. A
sound card with a microphone input has one, also when no microphone is
connected. Then Pi asks a running PulseAudio or PipeWire server, with pactl,
for a source that is not a monitor of an output. WSLg always lists its
RDPSource, so on WSLg setup starts when pactl (from pulseaudio-utils) is
installed. On macOS, a background check lists the input devices, and setup
starts when it finds one. This check needs Pi to run under a plain node
binary. Pi keeps the pactl answer and the macOS result for one day. When Pi
cannot find a microphone, or under other builds on macOS, setup waits for your
first dictation or /voice setup.
Background process. One daemon per user holds the model and serves every Pi session. It starts on the first dictation, exits 15 minutes after the last one, and exits sooner when no Pi session is open. The next dictation starts it again and loses no audio while it loads.
Logs. PI_VOICE_HOME keeps three local logs. provision.log records
setup, and daemon.log records the daemon. A daemon that cannot start (for
example, a Python file without execute permission) is also noted in
daemon.log, and the dictation shows the cause. voice.log records errors
that voice could not show, for example after /reload or /new.
macOS over SSH. macOS gives SSH sessions a silent microphone, so when Pi
runs over SSH, recording runs as a short-lived launchd job in your desktop
session instead. macOS asks one time, on the Mac's screen, to allow
ffmpeg to use the microphone; this needs Homebrew ffmpeg. About the first
half second of each recording is lost while that job starts.
The editor's top divider says what the agent is doing:
─ ⢌⡱⢎ Thinking… 00:12.4 ↓ 212 tokens ──── TPS 109.3 ─ TTFT 0.7s ─ Time 00:15.8 ─
- The word names the phase:
Sending request,Waiting for the model,Thinking(thenStill thinking,Thinking moreandDeep in thought),Writing main.md(a write; an edit readsEditing cc-phase.ts),Writing bash call,Running bashorRunning 3 tools, andWriting reply. A token count follows it while the model writes. Pi's own statuses (compacting, retrying, summarizing a branch) take its place while they run. - The spinner is three cells wide, so the word never moves. Each kind of work has its own: a ping while the request goes out, a helix while the model thinks, a print head while it writes a tool call, a comet orbit while tools run, two comets while it waits for a subagent's reply, and a wave that follows the stream's speed while it writes the reply. Compaction squeezes to a point, a retry drains, and a branch summary walks every row. With Tool Display's folded mode, only one place moves at a time: while a folded line is live, it shows the spinner. The status line then has no spinner and no space for one: its word starts right after the dash and holds still. Its words, clocks and colors stay.
- Color: the status turns amber after 10 seconds of thinking, and red when no tokens have come for 10 seconds. Some providers, Meridian among them, send the start of a tool call and hold its arguments until the model has written them all. While a call has started but none of its arguments came, the status stays calm for 2 minutes, as it does while it waits for a first token. When arguments come, the 10-second rule applies again.
- The clocks: a stopwatch beside the word shows how long the current step
has run, in tenths, in the word's color. It restarts for each new request,
thinking, tool call, tool run and reply;
Still thinkingand the later wordings keep counting.Timeon the right is the whole prompt. Pi's statuses get a stopwatch too, except a retry, whose own text counts down. Reduced motion stops the spinner but not the clocks. - TPS shows how fast tokens come. While the model streams, it counts the tokens that came in the latest second, and it changes on every frame (at least 12 times a second), so it rises and falls with the stream. Some providers hold a tool call back and send it in one piece at the end. Such a burst counts as if it came evenly over the silence before it, so it does not show as a false spike. When no tokens came in the latest second, TPS shows the prompt's average: the output tokens that the provider reported, divided by the time of the requests. The live count uses 4 characters a token, as the token count beside the word does.
- Narrow terminals drop TPS first, then TTFT, then the token count, then
Time. Then a long tool name shortens toWriting…orRunning…. The word, the spinner and the stopwatch go last, and the status never wraps.
While the model thinks, its newest three lines show dimly at the bottom of the conversation, above queued messages. New characters show brighter for a moment, then fade, unless motion is reduced. A tool call the model is still writing shows as Pi's own row in the conversation.
While a compaction runs, a purple band at the bottom of the conversation shows why it runs, the size of the context before it, and the time, with a sweep. With Cache Compaction, the band also counts the tokens of the summary so far and shows its newest 2 lines as they stream. When the compaction is done, the final compaction band in the conversation takes its place. A cancelled compaction turns the band gray for 2 seconds.
When a prompt finishes, a dotted π waves in and out in the divider, and the
conversation keeps an end line such as
π Worked for 41s, done 9:14 PM, avg TPS 98.1, or
π Stopped after 12s when you stopped it. A prompt of one minute or more
reads in minutes, for example π Worked for 2m04s, done 4:13 PM. The average
TPS shows when at least one response completed. The end line and the thinking rows
are only drawn; the model never sees them.
Set phaseSpinner.verbs to "playful" for pie and π words (Proofing,
Kneading, Approximating, …), or to your own "Present|Past" pairs. One verb
then lasts the whole prompt, the phase follows it, and the end line uses its
past tense.
Voice recording uses the bottom border while the agent works, so the status
stays visible. Pi's own working loader stays hidden.
/tool-display motion reduced holds every spinner on a still frame.
The π, ●, ∴ and timeout marks are drawn one cell wide. A terminal set to
draw East Asian ambiguous-width characters two cells wide will misalign them.
Tool Display redraws tool rows in the terminal: Pi's built-in tools, and every other tool too (see below). The tools are built as usual, and the model sees the same tools, descriptions and results; only the rows change.
Each call is one header band: the tool and its target on the left, the time on
the right. The band's color says how it went (green done, red failed, amber
timed out, gray aborted), so there are no status marks, and a failure is named
in words in the right rail (exit 1, timed out). While a call runs, the band
fills toward its timeout and warms as the timeout gets close; a call without a
timeout sweeps instead. Times read the same on every row: 8.6s, then
24m23s and 1h50m, and a timeout beside one is in the same units
(20m02s / 30m00s). Rows share the bullet column with the agent's text.
While a call runs, its bullet says what kind of call it is: a shell command
with a timeout fills ○ ◔ ◑ ◕ ● as it uses up its timeout, a subagent or peer
request has a slowly circling dot, a web call breathes, and other calls keep a
still dot. While the model writes a call, a pen moves in the bullet column, and
the right rail shows writing, the size of the arguments so far and the time,
for example writing 1,204 chars 6.2s. Under the band, the newest 2 lines
of the text the model writes now show, with a caret after the last. New
characters show brighter for a moment, then fade. The preview goes when the
arguments are complete. Some providers hold a call's arguments until the model
has written them all. Then the pen moves slowly and dimly, and the rail shows
only the time. Subagent and message rows show the same pen, rail and lines. A
call that waits its turn, or one rebuilt from history, shows a dim dot. Times of ten
seconds or more are drawn in a warmer color, so slow calls stand out when you
scroll back. Output sits indented under
the band on a gray panel, so each call reads as one block apart from the
conversation.
- bash: the command, and its last few lines of output.
- read: what was read, e.g.
80 linesor20 of 5,321 lines. - edit:
+12 −3, with long diffs collapsed. - write: the file's line count, and its last three lines (where a streaming write is).
- grep, find, ls: what was found (
23 matches in 7 files,42 files).
Every tool row stays visible after it finishes. Pi's native ctrl+o expands all rows, as before.
Output that doesn't fit ends in a line such as … 12 earlier lines; a single
hidden line is shown instead, since the hint would take its place anyway.
Click a row to open the whole call over the full terminal: the full command,
every line of output, and for a chained command each step (pick one with a
click, its number, tab or the arrow keys). copy command and copy output in
its title bar, or c and o, copy them whole; text dragged across copies
what it shows. Esc, q or ✕ closes it. ctrl+o still expands every row in
place.
Agent communication. Mesh agent_send and agent_request rows use purple
bands. Delivery failures keep their red rail words rather than recoloring the
band. list_peers keeps the usual operational colors.
Incoming remote-pi mesh messages show the sender's name and working-directory
basename, → me, and message or replies, over a purple Markdown preview.
Click or ctrl+o expands the body. Transport headers and reply instructions stay
in model context but are hidden from the human view; unfamiliar envelopes fall
back to a purple band over their raw text. Tool Display registers this renderer,
so it works even when Subagents is disabled.
Artifact feedback. An answer or a comment that the owner sends from the
artifact inbox arrives as a band, as a finished shell job does: ⇠, what came
in (answer, comment) and the page's title, with the count of fields in the
right rail. An answer's fields sit in a gutter under it, key and value side by
side, four while collapsed; a comment shows where it is on the page and what it
says. Click or ctrl+o shows all of it. A page that was deleted or expired gets
a gone band. A message with no changes in its details shows its text under
the band.
Other tools. Every other tool's rows get the same band, with the time and any failure in the right rail. pi-extras's own tools have layouts of their own:
- web_search, kagi_search: the query and how many results came back, with the first three under it.
- computer_use, windows_use: the apps (or VMs) the script used and how
many calls and screenshots it took, with the last calls under it. A failed
call says
failedornot allowedin words. - usage: each window's use in the band itself, amber from 80% and red when a limit is spent.
- artifact_publish, artifact_ask, artifact_check, artifact_feedback,
artifact_find (the artifact tools): the band says what the call does in a
word,
publish,ask,check,feedback,find artifactsorread artifact, and what it is about (ask Garden plan, 4 questions, private). Under it sit the page's address (the inbox link for a private page), when it expires and how its browser check went, one line each. Anaskrow shows the form and its page. Problems fold into… 2 problems; ctrl+o lists each one and where the check image is kept. The rows never draw an image: a check image the model got is the linecheck image sent to the model. Feedback rows count the answers and comments, and a search lists one line for each hit. With an older artifact extension, whose results have no details, a row shows the first lines of the result's text. The old tool names (artifact_new,artifact_answers,artifact_comments) get the usual band. - codemode: a JavaScript band with numbered
ƒtool-call cells, distinct from shell steps. While arguments stream, a bounded preview shows the newest four JavaScript source lines, or “Writing JavaScript…” until source arrives. It does not label draft calls as queued or executing. Live statuses and elapsed times come from Pi's nested-call events, never guesses about JavaScript statements.overlapmeans call lifetimes overlap, including queue and permission waits; it does not claim parallel execution. The collapsed row keeps the newest four calls. Expand or click for the full script, individual call output and script result. The popup keeps Source and Result views fixed and follows selected call IDs as calls arrive. The whole script scrolls in the body. Copy Script preserves ordinary source text but removes unsafe terminal controls, without changing execution; Copy Preview copies only retained call output. Missing or omitted previews cannot be copied. Truncating live output previews does not mark call history incomplete. Restored calls use Pi's saved metadata and say when nested output was not saved. Older/foreign implementations without call metadata retain their own result renderer./tool-display others offleaves codemode's original row;chains offaffects bash only. Script and tool behavior are unchanged.
Other extensions' tools, such as MCP, subagent and web access tools, keep their own words: the band shows the line the tool would draw for its call, and under it sit the first four lines of the tool's own result. A tool with nothing of its own to say shows its most telling argument and the result's text. The popup lists every argument and the whole result. Rows that already draw a band of their own, such as Shell Jobs, are left as they are.
Drawing other tools' rows relies on how Pi builds a tool row, which is not part of Pi's extension API. If a Pi update changes it, those rows are drawn by their own tools again; Pi's built-in tools keep the band either way.
Thinking. While the model thinks, its newest three lines show above queued
messages and the editor divider. Paragraphs and list items are joined with ·
rather than taking lines of their own, so the three lines hold as much as fits. When thinking
ends, the transcript keeps one ∴ Thought for 12s row. Thinking shorter than
one second shows tenths, such as ∴ Thought for 0.4s. Click the row to read the
whole block, and again to go back; ctrl+t does the same for every block.
/tool-display thinking collapsed shows just the label, as Pi does, and
/tool-display thinking full shows everything.
Folded mode. Folded mode is off by default. /tool-display folded on
turns it on. Then the transcript shows no tool rows and no thinking. Each run
of work between two replies becomes one line. The line hangs under the reply
that made the calls, set in two columns with no blank line between them:
● I'll find where the footer sets the model cell width.
● Ran 4 commands, read 1 file, edited footer.ts, ↑875k ↓1.4k 8.8s
The words count the calls by kind. Each step of a chained command counts as a
command, and the calls in a codemode script count in its place. When all the
edits (or all the writes) in a run are to one file, the line names that file
(edited footer.ts). The artifact tools have kinds too: published 2 pages,
asked 1 form, checked 1 page, searched artifacts 1 time and read the inbox 1 time; their old names count with the new ones. Tools without
a kind of their own, such as MCP tools, are counted together: used 44 tools,
or used 2 other tools after the known kinds. Only one such tool with a short
name is named (called usage 2 times). One failed call is named in red
(ls node_modules failed); two or more are counted (2 failed). When the
words do not fit the width,
the line shows the total count, for example ● Used 45 tools. After the words
come the tokens that the replies sent (↑, cache included) and received
(↓), and the time. The words are gray and the bullet and figures are a
darker gray. While the model works, the line is live and its figures count
up. The words tell only what the model did: a call counts as soon as it
shows, in the past tense, so the words never change when the run ends. The
phase spinner's own spinner, 3 cells wide, takes the bullet's place and
tells what the model does now: the ping while it waits for the model, the
helix while thinking text streams, the print head while it writes a call,
the comet while a call runs, and the two comets while every running call
waits on another agent. While the line is live, the phase spinner's status
line above the editor shows no spinner, so only one spinner moves. A line
under a reply has room for it in its margin,
so its words stay where they settle. A line at the left edge has no margin,
so its words start 2 columns further in while it is live. The words of a
live line keep the same gray, and a soft band of light crosses them from
left to right, as on the phase spinner's word. With
/tool-display motion reduced, the words and the spinner do not move. A run
with no calls yet shows no line until thinking text comes. Then the line
says how long the model has thought, for example Thought for 2.5s. A reply that thinks
and then writes keeps its thinking in a line above its words: the line of the
calls before it, or a line of its own directly above the words, such as
∴ Thought for 2.5s. That line looks like a finished thinking block in the
unfolded view and has no figures. The
live thinking tail
above the editor is not shown. Click a line to open its run. Its rows and
thinking blocks show inside a rule under the line, the line gets brighter, and
the rule ends with ╰─ close. Click the line or close to close the run. A
∴ Thought line opens to the full thinking text. ctrl+o opens every run.
The end line of a prompt is the same as in the unfolded view. The subagent
inspector is not folded.
Compaction. A purple header band shows the reason (auto, manual or
overflow), tokens before and estimated tokens after (~), cost when known,
and elapsed time. The first three summary lines sit under it on Pi's
compaction purple. Click the row or use ctrl+o to read the full Markdown summary.
Timing and sizes survive a session resume; older compactions show only their
before size. Pi's separate billing notice remains when enabled.
Chained commands. A bash command joined with &&, || or ; is shown as
its steps, each with its own status and time, so you can see which one failed
and which never ran. The running step's line breathes gently, and each step
that finishes flashes green (or red, or amber) and fades back, so a run of
quick steps reads as a wave down the list. A leading cd becomes the
location instead of a step.
To time each step, Tool Display adds a marker line around each step before the
command runs and removes the markers from the output before Pi or the model
sees it. The model's command and the output it reads are unchanged. Commands
Tool Display can't split safely run exactly as written. Examples are
heredocs, if, for, while and case blocks, function definitions and
background &. Others are exit, set and other commands that use the
shell's state, reads of $LINENO or $_, and any mention of alias or
eval. A pipeline continued on the next line and a list of more than 12
steps also run as written. See security and privacy for
the full list and for what the rewrite does.
/tool-display switches it:
/tool-display on|off: Tool Display's rows, or each tool's own./tool-display others on|off: draw other extensions' tool rows with the band, or leave them to their own renderers./tool-display chains on|off: break chained commands into steps, or run them as written./tool-display motion full|reduced: the reduced setting drops the sweep, the running step's breathing and the finish flashes. It holds spinners on a still frame and updates times every second./tool-display thinking tail|collapsed|full: how thinking blocks rest./tool-display folded on|off: one line for each run of work (see above)./tool-display count calls|steps: how Status Plus counts tools (see below).
The choices are saved in pi-extras.json. Rows change only in the terminal UI;
print, JSON and RPC runs keep Pi's tools untouched. If another extension
already replaces one of Pi's built-in tools, Tool Display leaves that tool's
definition alone and draws its rows as it does any other extension's.
Tool count. Status Plus counts tools as the folded lines do: each step
of a chained command, each call a codemode script made, and every other call
that ran as one. Subagents' calls count the same way. Where Tool Display saved
the steps a chain ran, the count uses them, so a chain that stopped early
counts what it ran; elsewhere it counts the steps in the command text. Calls
in a failed or aborted reply never run and are left out. Click the count in the footer to count one per
call instead, as Pi does, and click again to go back. The choice is saved.
Where the terminal sends no clicks to the footer, /tool-display count calls
does the same.
Shell Jobs are bands, like any call, and a running job's band fills with
its progress. A job is named after its title (Run unit tests becomes
run-unit-tests), and the model is asked to call it by its title when talking
to you. Starting a job leaves a still gray band in the transcript:
Run unit tests $ npm test ⇢ background, its title first, then the
command and status side by side. When the job ends the band takes how it ended and how long it
took (green ✓ exit 0 · 29.9s, red ✗ exit 2, gray ■ stopped), and its
completion repeats the band with the last output lines in a gutter under it;
click it, or ctrl+o, for the log and the rest.
Each running job has a live band above the editor. When the job's progress
can be known, the band fills with it: a command that starts with
sleep 30 fills over those 30 seconds, and a meter in its newest output line
fills it with what the meter says, as in
Download ISO $ curl -LO … 26.1s 45% · 32s left · 312M/690M · 12.1M/s.
The meters read are the ones tools draw into a log: curl, wget, rsync
(--info=progress2), git (--progress), tqdm, ninja, and pip or cargo when
they draw one, as well as any percentage drawn beside a bar. Time left reads
the same for all of them (32s left). A bare percentage counts only while it
climbs, so coverage 85% never fakes a bar, and a finished meter stops
filling, since the job may still have work after it. When nothing says how
far along it is, a sweep crosses the band and its latest output line shows
instead (▸ 212 passing). The spinner at its start dims
to a slow crawl when a log goes quiet. Four jobs fit; the rest give way to
+2 more. Click a job's band to open its live log over the full terminal,
with the same band at the top and buttons (or c and o) to copy the
command and the whole log; Esc, q or ✕ closes it.
Code blocks and quotes in the agent's replies are drawn as cards on a
background of their own. A code block's header shows its language and a
copy label; a quote's label sits at the right of a line near its top that
has room, or on a row of its own. Click a code block's header, or a quote's
label, and the block's exact text goes to the clipboard: the code as written,
tabs included, and a quote without its > markers or the line breaks
wrapping added. The label reads ✓ copied for a moment after. A card ten
rows or taller has a label at its foot too (a quote's last line, or a row
under it), so one is in view from whichever end you scrolled to.
Clicks need Pi's fullscreen mode ("tuiMode": "fullscreen"); in the regular
mode the terminal owns the mouse, so the cards keep their background and
leave the labels out. /copy-block works in both: it copies the last code
block or quote of the latest reply, and /copy-block 2 the second.
Drawing the cards relies on how Pi builds an assistant message, which is not
part of Pi's extension API. If a Pi update changes it, replies are drawn as
Pi draws them and /copy-block still works.
pull_link reads a link and gives the agent its content as markdown. Paste a
link into the prompt, and Pull Link reads it before the model starts (see
Auto-pull). The agent calls the tool for other links and for
more of a result.
| Link | What you get |
|---|---|
| X/Twitter | The post, its quoted post, media and polls, and the posts above it in the thread, through the public FxTwitter API. Replies need an X login, so they are not included. |
| Bluesky | The post, the posts above it and the replies (most liked first), through the public AppView API. |
| Threads | The post text and image from the page's preview tags. Replies need a login. |
| Mastodon | The post, the posts above it and the replies, through the server's public API. Any server with /@user/<id> links works. |
The post and its comment tree, through the JSON view of old.reddit.com. Share links (/s/...) work. |
|
| Hacker News | The story or comment and its comment tree, through the Algolia API. |
| The post and its top comments from the page's JSON-LD. | |
| GitHub | A repository and its README, issues and pull requests with comments, files, folders, commits, releases and gists. Set GITHUB_TOKEN or GH_TOKEN for more than 60 requests an hour. |
| YouTube, TikTok, Instagram, Vimeo, Twitch and other yt-dlp sites | Title, channel, date, counts, description, chapters, the thumbnail, a transcript with time stamps, top comments (comments) and frames on request (at, frames). |
An image (.jpg, .png, .webp, .gif, imgur, i.redd.it) |
The image. |
| Any other page | Title, author, date, description and the readable text of the page. |
The photos of a post come back as images. Image links in a post or comment
(imgur, i.redd.it and direct image files) also come back as images, up to 6
images in one result. An imgur album needs imgur's API, so it stays a link.
The tool gets the type and the size of each image from its bytes, not from the
server's label. It sends only JPEG, PNG, GIF and WebP files of 3.7 MB or less
that Pi's image library can decode. It makes an image larger than 2000 pixels
on a side smaller.
A video result is text and one thumbnail. It has no frames, so a long video
does not fill the context. The transcript ends with a hint: to see a moment
that the transcript mentions, call the tool again with at.
Arguments:
at: frames at exact times, for example["4:05", "1:02:30", "95"](up to 12). This call returns only the frames, with a short heading. Settranscript: trueto get the transcript too. A video of 10 minutes or less is downloaded one time at 720p and kept for later calls. For a longer video, the tool cuts a 2-second clip at each time, so it does not download the full video.frames: the number of frames to sample evenly over the video or therange(0 to 48). The tool sends 1 to 4 frames as separate images. It puts more frames on contact sheets of 12, each frame with its time stamp.- For
atandframes, the tool keeps each frame at full size, and the result gives the folder for thereadtool. range: a part of a video, for example12:30-15:00,90-or-0:45. The transcript and the sampled frames use only this part.comments: the number of replies or comments (default 20 for posts, 0 for videos).transcript: for a video link, the default is on. For a post that has a video (X, Bluesky, Mastodon), settranscript: trueorframesto read the video too.images: send photos, image links and the video thumbnail as images (default on).offset: a result longer than 40,000 characters comes in pages. The result gives the offset for the next page. The tool keeps the result for 30 minutes, so the next page does not send new requests.
The tool reads only public http and https links. It refuses local and
private addresses and links with credentials. It checks each redirect of its
own requests in the same way, and it sends a GitHub token only to GitHub's
API. It does not resolve DNS for its own requests, so a public name that
points to a private address passes.
Some sites refuse plain HTTP clients. When a page answers HTTP 403, 429 or
503, shows a bot check (for example Cloudflare "Just a moment..."), does not
answer in time or drops the connection, the tool reads the page again with a
local headless Chrome or Chromium. The result then starts with "Read with a
local browser" and gives the reason. A page that is not found (HTTP 404 or
410) stays a failure, and the error says "page not found". A 429 with a
Retry-After of 10 seconds or less gets one retry before the browser.
- The tool uses a browser that is already installed: Google Chrome or
Chromium from the system, then the Chromium of Playwright, then the
Playwright headless shell. When it finds no browser, the error gives the
command that installs one:
npx playwright install chromium(about 170 MB, no root). The tool does not download a browser by itself, because a blocked page in an auto-pull must not start a large download. - Each read uses a new temporary profile, which the tool removes after the read. The browser does not load images. It stops loading after 10 seconds, and the tool stops the browser and all its processes after 30 seconds or when you cancel. The page can be 8 MB at most. Two browsers run at the same time at most.
- The browser sends each request through a small proxy in the tool. The proxy resolves the name and refuses private, loopback, link-local and reserved addresses, also after a redirect.
- To turn the browser off or to use a different browser, set
linkContext.browser(see Settings).
The video tools (yt-dlp, its HTTP libraries and ffmpeg) send each HTTP request
through a small proxy in the media helper. This includes redirects, manifests
and fragments. The proxy resolves the name and refuses private, loopback,
link-local, metadata and reserved addresses. The tool does not download media
or captions on other protocols, such as ftp:// or rtmp://, and ffmpeg can
read only HTTP. yt-dlp does not start other downloaders, such as rtmpdump.
When you set linkContext.proxy or a proxy variable (HTTPS_PROXY,
HTTP_PROXY or ALL_PROXY), the guard still checks each request. Then it
sends the request to your proxy. linkContext.proxy comes before the
variables, and with it the tool ignores NO_PROXY. The variables follow the
rules of yt-dlp: HTTPS goes to HTTPS_PROXY, then HTTP_PROXY, then
ALL_PROXY, and plain HTTP goes to HTTP_PROXY, then ALL_PROXY. A host in
NO_PROXY gets a direct connection, and the guard checks it too. Your proxy
gets the IP address that the guard checked, not the name.
The guard finds each name with the DNS of this host, also for socks5h://.
DNS lookups do not go through your proxy, and .onion names do not work. A
proxy with rules for names sees only IP addresses. The guard cannot see
a name that is private only in the network of your proxy. An HTTP proxy can
get only a Basic login. A proxy URL without a scheme is http://, and
socks:// is socks4://. Other proxy URLs must start with http://,
https://, socks5://, socks5h://, socks4:// or socks4a://. If they do
not, the tool stops before it sends a request.
A shared YouTube link with &list= reads only its one video, not the
playlist.
When you send a message with links, Pull Link reads the links before the model
starts. It adds the results to the session as one message after your message.
The model then has the content in its first request, so it does not need a
pull_link call for these links. This saves one model turn for each message
with links. The cost is the size of the results in the context, also when the
model does not need them, and a wait before the model starts.
- Only messages that you type or send through RPC are read. Messages that an extension sends and the tasks of subagents are not.
- Auto-pull finds the
httpandhttpslinks in the message, also links in code spans and code blocks. It removes the punctuation after a link, such as.,,, quotes and a)that has no(in the link. It reads each link one time. It does not read local and private addresses or links with credentials. - It reads up to 5 links for each message. The message names the next 5
links and gives the number of the others. The model can read them with
pull_link. - Each result is the first 12,000 characters of what
pull_linkgives for the link. A note tells the model theoffsetfor the rest. - One message has up to 4 images in total (photos and thumbnails), in link order.
- A video gives its title, description, chapters and the transcript from its
captions. Auto-pull does not transcribe speech and sends no frames. For a
video without captions, the model can call
pull_link, which transcribes the speech. - All links are read at the same time. The model starts when all links are
read, or after 15 seconds. The message names a link that is still being
read. Its read continues for up to 5 minutes, and a
pull_linkcall for that link waits for this read and uses its result. - The results go into the
pull_linkcache. A link that was read in the last 30 minutes is not read again. - Each public link in a message gets a request, so a link that works only
one time (a password reset, an unsubscribe or a sign-in link) is used up.
Use
/autopull offbefore you send such a link. - The first video link installs the media tools (see Video tools), also when auto-pull reads it.
- A link that cannot be read is named with the reason. It does not stop the other links or your message.
- A message that you send while the agent works (a steer or a follow-up) gets its links in the same way. Pi adds the results at the end of the current turn, so they come before your message in the next request.
The transcript shows one row, for example
Pulled 2 links: x.com/user/status/… · github.com/owner/repo. Expand it to
see each link. Auto-pull uses the same network rules and the same guard as
pull_link.
To turn auto-pull off, set linkContext.autoPull to false. To change it for
the current session only, use /autopull on or /autopull off. /autopull
or /autopull status shows the current state.
The first video link installs the media tools into
~/.cache/pi-extras/link-context (or $XDG_CACHE_HOME/pi-extras/link-context,
or PI_LINK_CONTEXT_HOME). This takes 20 to 60 seconds. It needs git and
npm for the token provider. If they are missing, the tool tries again after
a day, and captions and other sites still work. It does not need sudo,
a browser or a desktop, so it also works on a headless Linux host. The tools are:
- A private Python (from
uv) withyt-dlp,youtube-transcript-api, a static ffmpeg (imageio-ffmpeg) and Pillow. The tool uses the system ffmpeg when there is one: the static build can crash on network input, which yt-dlp uses to cut clips. Pillow draws the time stamps on the frames, so no system fonts are necessary. - The bgutil PO-token provider, built with the Node that runs Pi. Without PO tokens, YouTube refuses most downloads and captions. The tool installs the latest release, and the yt-dlp plugin of the same version.
Video sites often change and stop old versions of these tools, so the tool
updates them every refreshDays days. If an update fails, the tool continues
to use the installed version. With PI_OFFLINE=1 (or true or yes), the
tool does not install, update or look up a release. It uses the installed
tools, and without them a video link fails with a note. But a speech model
that is not in the Hugging Face cache still downloads on the first
transcription. The token provider's npm ci runs with --ignore-scripts.
The transcript comes from the captions of the video. If the video has no
captions, the tool downloads the audio and transcribes it on this machine. It
uses mlx-whisper (whisper-large-v3-turbo) on Apple silicon and
faster-whisper (small, CPU) on other hosts. The first transcription installs
the package, and the package downloads the model from Hugging Face. The tool
does not pin or check the model, and this download does not go through the
guard proxy.
The tool does not download live streams. One download stops at 2 GiB of video
or 1 GiB of audio. When the length of a video is not known, the tool does not
download all of it or transcribe it. Give a range with an end to read a part.
YouTube blocks most datacenter IP addresses ("Sign in to confirm you're not a
bot"). Then the result has the title and channel from YouTube's oEmbed, the
captions if the caption client can still get them, and a note. On a cloud
host, set cookies to a cookies.txt file from a signed-in browser, or set
proxy. yt-dlp writes its cookie jar back when it stops, so the tool gives it
a private copy of the file in a temporary folder, not the file itself. The tool
removes the copy when the media helper stops, also when it kills the helper.
The install steps and the media commands get a minimal environment. It holds
the path, the home folder, the user name and shell, the locale and time zone,
and the temporary and XDG_ folders. It also holds the Hugging Face cache
folders and the proxy and certificate settings. They do not get other
variables, such as API keys. Only the install steps (tar, uv, pip, git,
npm and the TypeScript build) also get the UV_, PIP_, npm_config_ and
NPM_CONFIG_ settings, because a private package index needs them. yt-dlp,
the media helper and the token provider do not get a variable whose name
contains TOKEN, PASSWORD, AUTH, SECRET or KEY.
Put these in the linkContext section of pi-extras.json:
autoPull(defaulttrue): read the links in your messages before the model starts (see Auto-pull).refreshDays(default3): the days between updates of the video tools.0stops the scheduled updates. A new pi-extras version that changes the list of video packages still installs them again.proxy: a proxy URL for yt-dlp and the YouTube transcript client, for examplesocks5://host:1080. The URL can have a user name and password, for examplehttp://user:pass@host:8080(Basic login only). The guard checks each request before it goes to this proxy, and the tool ignoresNO_PROXY(see Pull Link).cookies: the path to a Netscapecookies.txtfile for yt-dlp.browser(defaulttrue): read pages that refuse plain HTTP with a local headless browser (see Pull Link).falseturns it off. A path, for example/usr/bin/chromium, uses that browser.asrModel: a different speech-to-text model, for examplemlx-community/whisper-small-mlxormedium.userAgents: a different list of user agents for an adapter, for example{"reddit": ["..."]}. The adapters arex,bluesky,threads,mastodon,reddit,hackernews,linkedinandweb. Each adapter tries its agents in sequence until a site gives a usable answer. Reddit sends a plain client to a login page but answers a link-preview agent, so the Reddit adapter triesfacebookexternalhitfirst.
The subagent tool starts a child agent: a separate Pi session on the model
the agent picks. It has a fresh context, the same working directory and the
same tools, minus a few that make no sense in a child. A child never gets mesh,
goal and shell job tools, computer_use, windows_use, usage, codemode,
tool_search, or other subagent extensions' tools. The full list is in
security and privacy. Children load the
extensions that provide their tools, Cache Compaction and a quota guard. Other
extensions, such as the remote-pi mesh, keep serving the parent session. It
runs in the background and its report arrives
as a message, so the agent keeps working or ends its turn and is woken when it
matters. wait: true blocks instead, for a quick check. readOnly: true keeps
only tools that can't change your files or run commands that the model
chooses: read, grep, find, ls, pull_link, kagi_search,
web_search, fetch_content, get_search_content and web_fetch. It
drops every other tool, also powershell and unknown extension and MCP tools. tools gives the child only the named tools
of the ones it would get, for example ["read", "grep", "web_search"]. It
always keeps message, and subagent and stop_subagent when it may start
subagents. The call is refused, with a list of the tools the child can have,
for an empty list, a name it can't have, or a tool outside that list with
readOnly: true. Its own subagents get no more tools than it has, the
subagents of a read-only child are read-only too, and a resume or a restore
keeps the same tools. context: "fork" gives the child a
condensed copy of the conversation up to the spawn, with mesh messages main
got. A relaunch gets the same copy, not what came later. cwd starts the child in another
directory, as an absolute path or one that starts with ~/, for example
{ task, cwd: "~/dev/app" }. Its tools, AGENTS.md and the paths it edits
then start from that directory. That directory's project settings and
resources (.pi/) load only as Pi would load them without a prompt: when
nothing there needs trust, when you trusted the directory, or when
defaultProjectTrust is "always". The directory must exist; links in its
path are resolved. The child's own subagents start there too unless they name
a cwd of their own. A resume or a restore keeps it.
Models. A child may run on any of the session's scoped models
(enabledModels, the list /scoped-models shows); short names such as luna
or opus work when they match one. Its thinking level is the call's, else the
one Pi's modelThinkingLevels sets for that model, else your default. These
settings come from the project's .pi/settings.json only when main trusts
the project. Which model suits what is yours to say in a Markdown guide,
~/.pi/agent/subagent-models.md:
- luna: extreme cost savings; bulk search, summaries, mechanical edits
- gpt-6.1 sol: reliable worker for implementation
- opus 5.5: taste; design, naming, reviewing plansIt goes into the tool's description with the model list. Pi reads it when a
session starts and on /reload, never mid-session, since a changed tool
description busts the prompt cache. /subagents guide edits it, or ask the
agent to.
Each launch of a child reads Pi's settings.json, the project's
.pi/settings.json and models.json again. It also reads the context files
(AGENTS.md), SYSTEM.md, APPEND_SYSTEM.md and skills that Pi gives a
child. The text of APPEND_SYSTEM.md comes before the child's own
instructions. A child in main's directory gets the project's .pi/ files and
settings only when main trusts the project. A child with a cwd of its own
gets them as described for cwd above. The child's extensions also read their
own sections of pi-extras.json, such as cacheCompaction and
rateLimitRecovery, at each launch. The subagents section applies at the
next session start or /reload, because it also sets main's tools. For
example, a change to compaction.reserveTokens or to a model's
contextWindow applies to the next child that starts. When models.json
changed, the launch first reads it into the model list that main and its
children share, as /model does. Before that, Pi's own loader checks the
file. If it finds an error, the launch uses the models from before and tells
you one time for each version of the file.
Talking. Every agent has a name made from its task, and main is the
session. message sends a note to an agent by name, or to all. A running
agent reads it after its current tool call; a finished one resumes with its
context to handle it. A failed or stopped one resumes too when main, its
parent or you write to it, and is told first how its last run ended; a peer's
note can't wake it. A child asks with expectReply: true. A question up the
tree, to main or its parent, waits for the answer, which comes fast because
the question wakes them. A question to anyone else, a peer or its own
subagent, returns immediately. The answer arrives as a message that wakes the
asker. The asker's report waits until every agent it asked answers or ends. One
that ends without answering, or can't resume to answer, hands the asker how
its run ended. Some agents wait for the asker, through their subagents or an
answer the asker owes them. A question to such an agent is refused; send a
note instead.
So agents never wait on each other in a loop. When a peer's message resumes a
finished child and that run ends normally, its report goes to its parent
without waking it: main reads it at its next turn, and a child parent at its
next run. A parent that waits only on its subagents wakes for it. A run that
fails or goes over its budget still wakes the parent, and so does one that
main, the parent or you wrote to while it ran. Main never waits: a child's note or question
wakes it, and so does the answer to anything main asked. When main's question resumes a finished child that
answers and then just writes its final text, the report doesn't wake main a
second time. More work, new input or a failure after the answer does, so a
report is never silently missed. Children know each other and can split work
directly.
For a review or fix loop, tell the agents to send messages to each other by
name. Tell them to report to you only at the end. The reviewer asks with
expectReply: true, so its report waits for the answer. A message from a
peer started the run of the other agent, so its report does not wake you. Two
agents can do many rounds, and main wakes one time, for the last report.
Messages to main show above the editor until they are in the transcript.
One that arrives while main works lands at its next turn boundary, and main
takes one more turn before it stops if it hasn't replied since. Esc doesn't
drop it: it stays in the transcript for main's next turn. While main
compacts, mail waits and wakes main after the compaction is done, so no turn
starts on the context being summarized. A /compact that stops main's turn
leaves the mail that landed in it unanswered. So after the compaction is done,
a hidden reminder that names who wrote wakes main to reply.
What you type to a child directly is recorded for main without waking it.
Reports from children started in the same run arrive together, as one message.
message call rows and incoming notes, questions, replies and relays read
like the conversation they are: ◆ reviewer → main asks over what it said.
A question keeps its amber asks word; failed deliveries keep red words.
Incoming mail has a Markdown preview, expandable by click or ctrl+o.
What main sees. Each message to main ends with one line about the other
agents that still work, for example
Still working: impl 37m $17.20, review queued after impl. This applies to
reports, notes, questions, answers and relays. For each
agent, the line gives the time and cost of its current run, and the agent it
waits for. It shows 8 agents at most, then +3 more. Subagents makes the line
when Pi gets the message, not when the message goes into the queue. When no
other agent works, the message has no line. The results of main's message
and stop_subagent calls end with Team now:. It gives the agents that work
or are interrupted, and the number of agents that ended. So main does not use
a turn to ask for the status of its team.
Seeing it. An agent reads like someone in a conversation, with no band
behind it: a ◆ and its name in its provider's color, the one the footer
gives that provider (Anthropic orange, Codex blue). Every other provider, such
as a local or self-hosted server, shares one mint, in the footer too. An
agent whose model names no provider is purple. What it is doing and the facts about it sit close beside them, and what it says set in under its name. Shell Jobs are
bands instead, so the two are easy to tell apart. A backgrounded start reads
◆ reviewer joined opus high working, with its task under it; the last word
says where it is (working, an amber question, finished · 1m12s, red
✗ failed), and expanding the row shows the whole task. Its report is the
agent speaking last: ◆ reviewer reported 1m12s opus, then the run's cost
and tokens (in counts cached prompt tokens too), and the first lines of the
report under it. When its answer is already on screen, the report stays one
line and a click unfolds its text.
Each agent has a row above the editor:
◆ reviewer 1m02s 48k 24% $0.21 opus high ∴ thinking, its name, how
long this run has taken, its context (the size after its last reply and that
size's share of the point where its session compacts), what it has cost, its
model and thinking level, then what it is doing right now. The compaction point
is the model's window less compaction.reserveTokens (or the reserve in
compaction.modelOverrides for the child's model), so 100% means Pi compacts
now. The percent can go past 100 until it does. With compaction off, or with a
reserve as large as the window, the percent is of the whole window. The point
is read at each launch. The percent turns amber above 70 and
red above 90, as the footer does for main's context. After a compaction the
size is ? until the next reply, and a finished agent keeps its last size,
which is where a resume starts. What it is doing comes last, so a narrow
terminal cuts it and keeps the other facts. Names, times, context figures,
costs and models line up, so what each agent is doing starts in one column.
After it has ended, ✓, ✗ or ■ takes the spinner's place. What it is doing
moves the way main's own spinner does for the same work, so the wait for its
model, thinking, writing, a tool call being written, a tool that runs, a wait on
another agent through a tool (these three in the tool color), a compaction
and a retry (in amber) look alike everywhere; a question waits in amber. The
words say the same: waiting for the model until its model sends thinking
text, words or a call, and again after its tools are done. Children of
children sit under their parent. A child the agent waits on (wait: true)
shows the same row in its subagent call instead.
Up to four agents show whole. A fifth turns the last row into a control line,
+7 more subagents · 1 asking · 6 working (view) (expand), so the rows stay
four lines and never say +1 more. The rows kept are the ones that need you
first (asking, failed, interrupted), then those still working; finished ones
give way first. (expand) shows as many rows as fit, up to all of them. It
takes at most half of what the editor and footer leave, so the transcript
keeps the rest. (collapse) takes them back. The rows stay expanded until
then, or until the widget empties. The widget empties when every agent is done
and its report has reached main. A terminal too short to give the rows more
room shows no (expand). (view), a click on +7 more subagents, or
/subagents opens every agent over the full terminal: the same live rows,
children under parents, each with its task under it and a count of what they
are doing on top. Use ↑↓ and Enter, or click either line, to open one; Esc in
its inspector comes back to the list.
Click a row, a subagent row, or pick one in the agents view to open the
inspector over the full terminal: the agent's chat, drawn as Pi draws main's.
Its task is the first message, then its replies, thinking and tool rows, with
the same look Tool Display, Copy Blocks and the thinking tail give main, and
the reply it is writing streams in. It uses main's hideThinkingBlock and
outputPad, from the project's settings only when main trusts the project.
A click on a row works as it does in main.
The chat sits on a wash of the agent's provider color, so it never reads as
main. Its live row is the title bar, and a message box sits between two rules
in the agent's color, as Pi's editor does; the top rule carries its tool
calls, runs, and whether it is read-only (its context is in the title bar).
What other agents, you or main told it reads as conversation rows, not as the
text its model reads:
→ reviewer you wrote or main asks over the message, ◆ finder → reviewer note in the sender's color, and ◆ lead-a reported for a report from one
of its own children. Type and press Enter to write to it (steered in while it
runs, resuming it when it has ended, answering it when it asked); ctrl+x
twice stops it, and Esc or ✕ closes. Main is told what you wrote. Status
Plus counts every child in its totals.
Reports. Each finished run that has a final report saves it in full beside
its session as <session base>.run-<n>.report.md, including failed and stopped
runs. The completion message gives that path and a preview of up to 12,000
characters; main reads the rest with its normal read tool. /subagents report <name> shows the latest report's path. Each run keeps its own file, and run
numbers can skip when a run ends waiting for a reply. If saving fails, the
message still arrives and points to the session file as before.
Checks. check on subagent or message is a shell command, for example
{ task, check: "npm test", fixUntilPass: 2 }. When a run of the child ends
normally, the harness runs the command before the report goes to the parent.
The command runs through the child's own bash tool, in the child's
directory, with the same shell settings and worktree sandbox. It gets no
input. A timeout stops it after 10 minutes, or after checkTimeoutMinutes (60
at most). The run stays live while the check runs, and the row of the child
shows check: npm test. Then the report ends with the command, the exit code,
the time and the last lines of the output. For a failure, these are 40 lines
or 4,000 characters at most. For a pass, they are 10 lines or 1,000 characters
at most. The report file has the same text. So the parent gets data from the
harness, not only what the child says. A report with a check that did not pass
always wakes the parent. With fixUntilPass (0 to 5, default 0), a failed
check goes back to the child as more of the same run. Messages that came
during the check go with it. The child repairs the cause, and the check runs
again. The run keeps its budget, and the parent gets only the last result. The
fix rounds count across the whole run, also when the run waits for its
subagents or for an answer. The next run starts again at 0. A check that could
not run gets no fix rounds, because the child can't repair the harness. A run
that fails, is stopped or goes over its budget gets no check. A run that
called no tool that can change files since the last check also gets no check,
for example a run that only answered a question or sent messages. The tools
that can't change files are read, grep, find, ls, message and
stop_subagent. All other tools count as changes. This includes subagent
and each tool that Subagents does not know. Then the report ends with Check skipped: no changes since the last check. A change by a subagent counts for
each agent above it, also when a message resumed that subagent. The harness
counts each call when it starts, so a compaction does not hide a change. The
check runs again when its command changed or the last check could not run.
After a restart, the next check always runs. The child keeps its check for its
later runs. A message with check replaces it. A refused message leaves
the check as it was. No message removes a check; to make it pass always, set
check: "true". When that message does not
give fixUntilPass or checkTimeoutMinutes, the old values stay. Only main,
or a child that can run bash itself, can set a check. A message can set a
check only on one of the sender's own subagents. A read-only child can have a
check, but not fixUntilPass.
When a run ends normally, changes files and has no check, its report ends
with Not checked: this run changed files and had no check command. Verify the work before you rely on it. A change is an edit or write call, or a
bash command that the edit lock saw change files in a git work tree.
Stopping. stop_subagent stops a subagent and everything it started:
main can stop any, and a child that may start subagents can stop its own.
The call returns how it ended and its last message, so no report follows; a
message resumes it later. /subagents stop <name>, stop all and ctrl+x in
the inspector stop children for you, and then main gets the stopped report.
A stop ends spawns that have not started first, so a spawn that waits for a
stopped agent never starts. A stopping agent can't start new subagents. An
agent that fails stops its subagents too. Each of their reports goes to the
nearest agent above that still runs, or to main, so no report is lost.
Order. after on subagent gives the agents that must end before the new
child starts, for example { task, after: ["impl"] }. The call records the
child and returns. wait: true can't go with after, because the agents
before the child could need an answer from the caller. The child stays queued
until each of these agents is idle, failed, stopped or interrupted. It does
not use a maxConcurrent slot while it waits, and its row shows after impl.
Its time budget starts when it starts. Its first prompt then tells how each
agent ended and gives the path of each report, but not the report text. It
also tells when an agent finished but its check did not pass. Each name must
be an agent of the team. A name is refused when that agent can't end before
the agent that starts the child, because each would wait for the other.
Examples are the agent that starts the child, an agent above it, and a queued
child that waits for one of these. index.json keeps the names, so a restore
or a /reload keeps the wait. A restore resumes a queued child after the
agents it waits for. So when one of these agents resumes too, the child waits
for it again. An agent that stays paused counts as ended. Only the first run
waits; a resume does not. A stop removes a queued child before it starts. When
a run ends normally and a queued child of the same parent waits for it, its
report does not wake the parent. The parent reads it with the report of the
queued child. A run that fails, is stopped or has a failed check still wakes
the parent. A child can't ask a queued child a question, because the answer
can come only after the start. Such a question could also make a loop of
agents that wait for each other. A note is possible, and the queued child
reads it when it starts. For the same reason, a child can't ask a question to
an agent that waits for it through a queued child. For example, lead waits
for its subagent docs, and docs starts after impl. Then impl can't ask
lead a question: lead could end its run before it answers, and each agent
would wait for the other. A queued child does not keep the reports of its
group waiting.
Writers. The first child to edit files in a checkout holds that checkout
until its run ends. The checkout is the git work tree that contains the edited
file: a repository or one of its worktrees. For a file outside any git work
tree, it is the child's own checkout: its worktree, else git's top level of its
cwd, else that directory itself. Children without a cwd share the parent's
checkout for these files. Paths are compared after links are resolved, so two
paths to one directory are one checkout. So children that edit different
repositories, worktrees or directories never block each other, even when the
parent works in a directory that is not a repository. Outside git the lock
follows the child, not the file: a child with cwd: "~/notes" and a child
without a cwd in ~ do not block each other, even for the same file. The holder keeps the checkout
while it waits on its own subagents, and lets it go when it finishes, fails, is
stopped or is interrupted. Its ancestors and
its own subagents can edit beside it. Any other child's edit or write call
there is refused before it runs, with a message that names the holder and the
checkout. That
child can then do work that does not edit files, or ask its parent for a
worktree. Main never takes or checks the lock. A child's bash command can't
be checked before it runs, so the check comes after it: the child's git status
(with the size and modification time of each listed file) is compared before
and after each call. Ignored files do not count. A command that changed files
takes the lock of the child's work tree as an edit would. When another child
holds it, the changes stay, the command's result tells the child to stop and ask
for a worktree, and you get a warning. This is detection after the fact, not
prevention. Files that an
edit or write call of any agent, main included, targets during the command
do not count. When another agent's bash command runs in the same place at the
same time, the changes can't be told apart, so that call is not checked. The
same place means the same git work tree, from any directory in it. Outside
a git repository, or when git fails or takes more than 10 seconds, calls are
not checked, and you get one warning. Use isolation: "worktree" for
parallel edits. Each worktree has its own lock, shared by the helpers that
inherit it. A worktree child's edit and write calls outside its worktree
are refused before they run, and the check runs again as each call starts. So
a link that an earlier call of the same reply made can't take a path out. The
check follows symlinks, so an edit through the
worktree's node_modules link into the parent's checkout is refused too. A
worktree child's bash commands run in an OS sandbox that blocks writes into
the parent's checkout (and, for a worktree made from a worktree, into each
checkout it came from). Some exceptions stay writable, so git add,
git commit and git status work in the worktree. In the repository's .git,
these are the worktree's own folder in worktrees/ and objects/. They are
also the ref and reflog of the child's own subagent/<name> branch. The
hooks, the config, info/ and the other branches stay read-only. In
node_modules, only .cache is writable, so that tools such as babel-loader
and webpack can keep their caches. Note that a cache can hold build output
that your own tools run later. The packages, .bin and the other dot-entries are not writable, so
an install in a worktree can't change the parent's dependencies.
The sandbox stops unintended writes into the parent's checkout. It is not a
boundary against a hostile child: the child can still write outside the
repository, for example ~/.gitconfig. When a command fails on a blocked
write, its result names the parent's checkout. The transcript shows the child's own command, not the sandbox around
it. macOS uses sandbox-exec. Linux uses bwrap when it is on PATH. There,
only the paths that exist when the child starts are writable. The child can
also change the other subagent/ branches. Without bwrap, and on Windows, there is no sandbox: the boundary for
bash is only the child's working directory, which is the worktree, and its
instructions. Worktree isolation
needs a git repository: the one at the child's cwd, else the one the parent
works in. So { task, cwd: "~/dev/app", isolation: "worktree" } works when
the parent's directory is not a repository. The child starts in the same
directory of its worktree as its cwd (or, for a subagent's own worktree, its
parent's) is in the repository. A read-only
child's subagents can't make a worktree from a cwd, because making one runs
that repository's git hooks. A worktree child's subagents can name only a cwd
inside that child's worktree. It copies that repository's current files, including uncommitted
changes and untracked files that Git does not ignore, into a separate checkout.
The parent keeps its files and index unchanged. The report gives the worktree,
branch, change counts, and commands to apply changes and remove it after merging.
The apply command runs git only in the parent's checkout, on a snapshot of the
worktree's files that main wrote when the child ended.
A child can end before its subagents. Then their reports give their own apply
commands, so their later changes are not lost.
Untracked node_modules are linked, not copied or included in the changes.
Worktrees stay until you remove them. Resumed children keep the same workspace.
After you apply the changes, the cleanup command force-removes the worktree and
deletes its branch. Check that you kept all needed files first.
Limits. At most maxConcurrent children run at the same time; the rest
queue. The limit also applies to a child that goes on in its open session.
Examples are a resumed idle child, a woken parent, and a child that got its
answer. Such a child waits in line ahead of spawns that have not started. A subagent that its
parent starts with wait: true runs in that parent's slot. A child can't
start children of its own by default.
Each run of a child has a time budget, maxRunMinutes (default 60), and an
optional cost budget, maxRunCost (no limit by default). The subagent call
can set its own with maxMinutes and maxCost, and the child keeps them
when it resumes. Time counts only while the run works, not while it waits
for an answer, for its subagents, or for a free slot. A run that wakes from
waiting on its subagents keeps what it used. Cost counts only what this run
spent. Each agent has its own budget, which its parent's budget does not
limit. A child can give a subagent a larger budget than its own.
The child's instructions name its budgets. At 80% of a budget, the child gets
one note that tells it how much is left, so it can plan its report. A time
budget under 5 minutes gets no note. A run that goes on (on a fallback model,
in a new session, or after a wait) does not get the note again.
At the limit, the run must wrap up. Each new tool call is blocked, and the
widget shows wrapping up: over its 60-minute budget. The child's own
subagents stop without a report. A tool call that already runs gets 60 seconds
to finish; after that, the run is aborted. At the next turn boundary, the child
is asked in the same session for its final report: what is done, what is not
done or not verified, what it leaves behind, and the next steps. The prompt
names the subagents that were stopped. If its reply at the limit called no
tool and no subagent was stopped, that reply is the report. The run then ends
as a normal run does, and the child keeps its context. Its parent gets a report
whose header says, for example, reached its 60-minute budget after 1h00m and wrote this report when told to stop. A report to main wakes it. The check
does not run, and the run gets no fallback model and no move to a new session.
Mail that comes during the wrap-up waits for the next run. The run can go over
maxCost by a few replies at most: the reply that streams at the limit, the
wrap-up reply, and each blocked reply. A run whose last reply crosses
maxCost as it ends also counts as one that reached its budget, so its check
does not run.
If 2 replies after the wrap-up prompt still call tools (both are blocked), if
the model fails, or if the run has not ended 4 minutes after the limit, the
run is stopped with its subagents, like stop_subagent. Its parent then gets
a report that says why, for example stopped after 1h04m: over its 60-minute budget,
with its last message. So a hung child can't keep a pi -p run open forever.
A message to the child resumes it with a fresh budget, and it is told how its
last run ended.
Fallback. Sometimes a child's model can't serve its run: its quota or
usage limit is used up, a rate limit lasts longer than the automatic wait,
the provider is overloaded or unavailable, the Claude Code proxy (Meridian)
ends the turn early after its retries, the credentials are missing or
expired, or the model is gone. Then the run does not fail. It continues from
the child's session on the next model of fallbackModels, by default the
default subagent model. The child is told first that its model failed and
why, and to check the files before it continues. The fallback skips the model
that failed and models outside the scoped models. After a quota, usage-limit
or credentials failure, it also skips the other models of that provider. Each
model gets at most one try per run. The row shows the new model, and the
report says what happened, for example Ran on anthropic/claude-opus-5-5 after openai-codex/gpt-6.1-sol failed: usage limit reached. A fallback model can
cost more than the model main chose. The run keeps its budget: the time and
cost it used before the switch still count. When no model is left, the run
fails and its error says so. Other failures, such as a full context or a bad
request, fail as before.
Memory. A child's session is released when it ends. It is released
immediately when it failed or was stopped. When it finished, it is released
after two quiet minutes, since a follow-up often comes soon. A message to a
released child relaunches it from its session file with its context, and its
inspector reads that file, so a long session with many children keeps only the
working ones in memory. A
message within the two minutes uses the open session only when a launch now
would make the same session. After a /reload, or after a change to a file
that the launch read, the child relaunches from its session file instead. A
finished child whose session is not current releases it immediately.
Moving to a new session. A working child whose session is not current
moves to a new session at its next turn boundary. That is the point after a
turn, when the results of its tool calls are recorded and before its model
gets the next request. The cause is a /reload or a change to a file that the
launch read. These files are settings.json, the project's
.pi/settings.json, models.json, pi-extras.json, the context files,
SYSTEM.md, APPEND_SYSTEM.md and the skills. The new session opens from the
session file with the current code and settings, and the run continues as the
same run. It keeps its run number, its time and cost budget, its check and fix
rounds, its edit lock and the answers it is owed. Messages that it had not
read go to the new session with a short note. The note tells the child that
its session moved and that it continues where it stopped. A child does not
move while a question with expectReply or a subagent call with wait: true
blocks it. It moves at the end of the turn after that call returns. A child
also does not move while its run stops, or when its model is not in scope now.
Each cause moves a run one time at most. Changes to files move a run only one
time, and later changes wait for its next run. This is because Pi writes
settings.json each time you change main's model or thinking level, and a
child can edit a context file many times. New code after a /reload still
moves the run. Its row shows moving to a new session and the cause during
the move. Each move adds a line to ~/.pi/agent/subagents/swaps.log. Pi adds
a changed system prompt after the earlier messages, so the prompt cache keeps
the prefix. A move after a code change can cost one cache miss when the new
code changes the child's tools. swapAtTurn: false in the subagents section
turns moves off.
Headless runs. pi -p and --mode json end when main settles, so main
waits there for its background children. Their reports, questions and notes
go into main's context as it would otherwise settle, with one more turn to
read them; a child's question to main gets main's answer and the child goes
on. Main settles when no child is working and nothing waits for it.
Reload. /reload does not stop children. A child that works moves to the
new code at its next turn boundary, as the same run (see Moving to a new
session). A child that starts after the reload uses the new code. A child
that reaches a turn boundary during the reload waits for the new code. Mail to
main waits during the reload and then arrives one time. When no child works
and all mail is in main's transcript, a team from the new code takes over from
index.json. Until then, team settings from pi-extras.json, such as
maxConcurrent, run budgets, batchMs and swapAtTurn, stay as they were.
Children get new code of an extension at the latest after /reload. They also
get a new context file or skill at the latest after /reload.
Some reloads must interrupt children:
- a new version of pi-extras that can't take over from the old one
- a takeover that fails, or a reload that can't keep the children
- a reload that does not finish in 30 seconds
Then Subagents interrupts the children and records them in index.json. The
next session start resumes them, as resumePolicy allows. Mail to main that
waited during the reload stays in index.json, and the next session start
sends it one time, after its restore notice. A question from a child that
stays paused does not wake main. Under resumePolicy: "notify", main's answer
to it goes to the child's inbox and does not resume the child. A reload that
does not finish, or that can't keep the children, writes a line to
~/.pi/agent/subagents/reload.log. The warnings of that interruption go to the
same file, because no session can show them. For the other cases, Pi shows a
notice.
Restoration. Children survive /reload, restarts and crashes. An
index.json beside their sessions keeps each child's name, settings, state and
run count, so message reaches them again after a restart. It also keeps mail
that has not reached a child yet: notes in its inbox, the answers it is owed,
and messages steered into its run that it had not read. After a restart, its
next run gets that mail first, and an answer it is owed still resumes it.
A run that fails or is stopped keeps its unread messages for the next run
too. Children that a
/reload interrupts resume on their own, one time. After a restart or crash they
come back paused, and main gets one notice listing them with their task and
last activity. /subagents resume <name> continues a paused child; message
finished ones instead. A resumed child is told to check files first, because
its last tool call may not have finished.
Some children never resume on their own: ones paused earlier, grandchildren,
sessions from before 0.15.0, orphans (child sessions missing from the index,
listed in /subagents and restored read-only), children whose workspace moved,
and children whose model is no longer allowed (resuming one of those by hand
uses the current default model and says so). An automatic resume that is
itself interrupted stays paused, and a failed resume can be retried. Only one
Pi process at a time can own a session's children; forks start without them.
Each run adds a line to ~/.pi/agent/subagents/runs.jsonl (model,
task opening, time, tool calls, cost, how it ended); /subagents stats sums it
by model, which is what to tune the guide on. A run that wrote its report at
its budget has a budgetEnd field, and stats count it as at budget, not as
an ordinary finish. Each line holds that run's own
tool calls and cost ("format": 2). Older lines, with no format, hold the
child's totals so far, so stats over them count a child with many runs too
high. /subagents stop <name> or
stop all also stops paused interrupted children.
If another extension already has a subagent tool, such as pi-subagents,
Subagents stays off for the session and says so; remove one of the two. A
message tool from another extension is left alone and only that tool is
skipped.
Opt-in and macOS only: set PI_COMPUTER_USE=on before starting Pi. It adds a
computer_use tool that runs a short script against OpenAI's Computer Use
methods (sky.list_apps, sky.get_app_state, sky.click, sky.type_text and
the rest), so the agent can read an app's accessibility tree and screenshot and
act on it. It needs the ChatGPT app for macOS with Computer Use turned on, which
installs the signed Computer Use client under ~/.codex/computer-use, and
someone logged in to the Mac's desktop. /computer-use shows what is missing.
The first use of each app asks you to allow it. "Don't allow" is the default,
so a stray Enter refuses. "Allow for this session" lasts until the client exits;
"Always allow" is stored by the Computer Use service and also applies to ChatGPT
and Codex computer use. Apps the service rates high risk, such as browsers, show
its prompt injection warning. The service never allows some apps, such as
terminals. Without a UI, such as pi -p, an app that is not always allowed
is denied unless Allow all is on. The agent is then told how to allow it.
Subagents never get computer_use or windows_use.
/computer-use opens a panel with the client's status, the apps mode and a
checklist of the apps the agent may always use. Check and uncheck any number of
apps, type to filter, and press Enter to save them together; always allowing an
app or turning on Allow all asks you to confirm first. Checking an app ahead of
time lets headless runs use it. Changes apply immediately, including to a
running client. The checklist edits the same approvals file as the ChatGPT
app's settings; if that file ever has a format this version does not know, it
is shown read-only.
The apps mode sits on top of the checklist, applies to every Pi session, and
is kept in ~/.pi/agent/pi-extras.json:
- Ask per app (default): checked apps are used without asking; any other app asks you first.
- Allow all: every app is allowed without asking, including high-risk apps such as browsers and mail, and also without a UI. These approvals last only for the client session and are never stored, so the checklist is unchanged when you switch back. The service still refuses some apps, such as terminals.
- Allow none: every computer use call is refused, even for checked apps.
Each tool call shows the script as highlighted code, then every Computer Use
call it made, with its app and target, how long it took, and any approval.
The client stays running between calls, so element indices stay valid across
turns, and exits after five idle minutes. It runs as a launchd job in your
desktop session, so it works the same from a local terminal and over SSH. Use
only one computer_use provider at a time; remove another computer-use
extension before opting in.
This relies on undocumented parts of the ChatGPT app and can break when it updates. OpenAI does not produce or endorse this integration.
Opt-in, for Pi running in WSL on a Windows Hyper-V host: set
PI_WINDOWS_USE=on before starting Pi. It adds a windows_use tool that runs a
short script against the host's Windows VMs, the way computer_use runs one
against Mac apps. Guest methods (win.snapshot, win.click, win.type,
win.key, win.app, win.powershell and the rest) go to
Windows-MCP inside the VM, which
reads the UI Automation tree and acts in the signed-in desktop. win.console.*
methods drive the VM's screen, keyboard and mouse from the host through
Hyper-V in a confirmed console session, including its lock and UAC screens.
In an enhanced VM Connect/RDP session, guest methods operate in that same
session, console input is refused, and console screenshot/OCR methods read
Windows-MCP's image instead. Every method names its
VM: win.snapshot({ vm: "Win11" }); win.vms() lists them. To keep a session
away from some VMs, set PI_WINDOWS_USE_VMS to the ones it may use, such as
PI_WINDOWS_USE_VMS="Win11,Test Lab" (names match case-insensitively). Other
VMs are then left out of win.vms(), and a call naming one fails before
anything reaches the host.
On a visible basic-session desktop, the first call installs Windows-MCP
through the console. An enhanced session needs Windows-MCP already installed
and reachable; a sign-in screen alone is not evidence that console sign-in is
safe. Setup opens an elevated PowerShell from the Run box,
accepts the UAC prompt, reads the console with OCR until that PowerShell is
ready (so the bootstrap, which carries the key, is never typed into another
window), and types a short bootstrap that installs
uv and Windows-MCP (0.8.6 or a later 0.8
release, whose output windows_use is built to read) for the signed-in user,
starts it at every logon, opens its port to the local subnet only, and
requires a random key held on the host, new for every install. Setup disables
PSReadLine only in its temporary shell to avoid expensive long-line redraws.
The host key changes atomically after the entire command is queued, so a
partial-typing failure leaves the existing server's authentication usable.
Console input uses a paired Hyper-V TypeKey stroke per character with settled,
grouped modifiers to preserve case and punctuation without a separate CIM call
for every make and break.
When the VM's Hyper-V key-value exchange works (it's on by default), the
bootstrap itself goes over it and only a short stub is typed. The stub carries
the key and the bootstrap's hash, and a whole setup took about three minutes
live. Otherwise all of it is typed, which took about seven minutes, within a
10-minute input budget. Installation needs internet access in the guest. The
exception is a reinstall that can't reach the package index: it keeps an
installed Windows-MCP from the same release line. An install that fails
stops immediately with the guest's error, which the bootstrap reports to the host
through Hyper-V key-value exchange. After that, each call first makes sure the VM
is usable and repairs what it can, and the result says what it did:
- The server reports its live Windows session and connection state before guest tools run. A locked, disconnected or transitioning remote session needs the user to reconnect or unlock the same VM Connect window.
- A locked console session is signed back in only after a fresh session check. After a reboot, the tool waits for the logon task. If the server remains unavailable at sign-in, it asks for help instead of taking the console.
- A stopped server may be reinstalled when a console desktop is visible. A remembered enhanced session blocks this fallback even if it goes offline.
- An unresponsive server may be restarted from a confirmed console's Run box, read with OCR first. In an enhanced session, it reports the failure without sending console input.
- A snapshot stalled by Start or its search, which sometimes stop answering UI Automation, gets them restarted (Windows starts them again when opened) and is taken again. A snapshot stalled by another app's window fails after 30 seconds and names that window.
- A VM that is starting, restarting or installing updates is waited for, up to 15 minutes, before anything is clicked. It shows as a nearly black screen or a missing Hyper-V heartbeat.
- A VM that is off or saved is left alone;
win.start({ vm })starts it.
Windows-MCP runs with the signed-in user's rights, not an administrator's, so it can't read or send input to the windows of apps that run as administrator: their UI tree is empty, and its clicks and keys to them are dropped without an error. Snapshots mark such windows. Console input can reach them only in a confirmed basic session; an enhanced session cannot use that fallback.
To give Windows-MCP administrator rights, set PI_WINDOWS_USE_ELEVATED=on.
Setup then has the logon task run it with the guest user's full
administrator rights (the task's highest run level, which needs that user to
be an administrator), and a server set up the other way is reinstalled on
the next call at the console; turning the setting off reinstalls it without
them. In an enhanced session a mismatch is reported, not repaired through the
console. Match the setting to the existing server or arrange a repair without
moving the desktop.
win.powershell and the apps win.app launches then run as administrator,
without UAC prompts, and Windows-MCP's clicks and keys reach apps running as
administrator. Their UI trees fared poorly in testing even so: a snapshot
crashed Event Viewer whenever it showed an event log, Services stalled
snapshots past their limit, and a vendor's MMC console kept its tree hidden. The
tool description steers agents to PowerShell (Get-WinEvent,
Get-Service) and OCR for such consoles. See
security and privacy for what the setting
allows.
win.console.ocr({ vm }) reads the screen's text with Windows OCR on the
host, as lines of (x,y) text whose centers can be clicked. It covers what
the UI tree can't describe, such as custom-drawn windows, MMC consoles, UAC
and sign-in screens at the console, and needs no model that takes images.
In enhanced sessions it reads the guest screenshot and converts OCR points
back to desktop pixels, accounting for image downscaling and monitor offsets.
Region filters use those same desktop pixels. win.console.screenshot reports
the image dimensions plus the enhanced image's source, session, x/y
origin, screenWidth/screenHeight and scaleX/scaleY; screen coordinates
are origin plus image coordinates times scale. Failed remote captures never
fall back to the unrelated console. Only a freshly confirmed console display
may be woken with Shift; an unknown console screenshot stays read-only.
An app that asks for administrator rights raises a UAC prompt on the secure
desktop, where Windows-MCP can't see: a snapshot then fails and says a prompt
is up. At a freshly confirmed console, win.uac({ vm, answer: "yes" }) (or
"no") answers it. In an enhanced session the user must answer in VM Connect;
console keys cannot reach that session's secure desktop. A prompt that asks
for a password stays up; it is never typed. A synchronous PowerShell
Start-Process -Verb RunAs can wait for consent and time out. Commands are not
rewritten or automatically repeated to work around that.
A call that never reached the server is sent again after the repair. One whose
connection dropped mid-way is not, since it may have run (a Restart-Computer,
say); its error says so, and the next call reconnects.
At a confirmed console, signing in clicks the last-used account's Sign in
button, which suits passwordless lab accounts. The taskbar check still avoids
clicking an unlocked desktop, and at most two sign-in clicks are attempted.
Session checks use Windows WTS APIs, not an inherited SESSIONNAME or localized
command output. win.login, win.setup, UAC and console input all share the
session guard; none is an override. If the server is unavailable and no console
desktop is visible, even probe/wake keys are withheld. This deliberately gives
up unattended sign-in in an ambiguous case rather than disconnecting a user's
remote desktop or VPN.
It needs powershell.exe through WSL interop and a Windows user in the
Hyper-V Administrators group. Guests need a US keyboard layout for console
typing. The host side runs as one Windows PowerShell process, started on the
first call and closed after ten idle minutes; its first call takes a few
seconds. /windows-use lists the host's VMs and which are set up.
To watch the agent work, keep the intended VM Connect window open and visible, in either basic or enhanced mode. Windows can move the existing desktop and Windows-MCP process into an enhanced session; it is not necessarily a separate user login. Avoid simultaneous mouse/keyboard input. Do not switch sessions just to repair a failure: moving the desktop can disconnect a VPN.
Calls reach Windows-MCP over a Hyper-V socket, not the guest's network. A
small relay in the guest (Python standard library only, run by the signed-in
user from %USERPROFILE%\.windows-mcp\guest-relay.py) takes them from the
host and forwards them to the server on the guest's loopback. So a VPN in the
guest that captures all its traffic, or firewall rules that cut the guest off
from the host, leave windows_use working. When the installer comes over
key-value exchange, setup installs the relay with the server. Otherwise the
first call that reaches the server over the guest's IP installs it. From then
on, the relay also restarts a stopped or stalled server and reports the
desktop session, without the console. A relay that stops is started again
within a minute or two by its scheduled task. A relay update that fails to
start is rolled back to the previous version. Without a relay, calls use the guest's IP
and port as before. The relay changes no guest credentials or password policy.
Live, the test guest ran a full-tunnel VPN that cut the host's route to it. Normal work, a new Pi session, a stalled server and a stopped one, a VPN reconnect, a reboot with the VPN up at boot, a locked console, and a relay upgrade (plus a failed one, rolled back) all worked, with nothing sent to the guest's IP. So did a first setup with the VPN already up and no relay in the guest: it took four and a half minutes through the console and key-value exchange alone, and kept the installed Windows-MCP because the VPN blocked the internet. Warm calls took 130–180 ms, and a new session's first call took about 3 seconds.
Earlier live validation confirmed same-session
input, UIA, screenshots, OCR-coordinate clicks, disconnected-session refusals,
and no console takeover during a server outage. Live screenshot and OCR-click
checks passed at 1366x768 and at 2560x1440 with screenshots downscaled to
1920x1080. OCR boxes are rotated back from Windows' detected text angle before
conversion to native coordinates. A timed restart of the existing logon task
restored the server in the same enhanced session; this was a test helper, not
automatic remote repair. Live basic-session lock/sign-in and unresponsive-
server Run-box recovery also passed. After replacing separate scancode events
with paired TypeKey strokes, full basic-session bootstrap passed in about
7 minutes 24 seconds, retaining the same unlocked session and limited server.
A forced partial-typing timeout preserved the original authenticated server.
Post-bootstrap stalled-server recovery passed in 90 seconds without reinstalling.
Without a relay, network timeouts can delay outage detection and recovery.
This integration uses a Kagi subscription session token, not the paid Search API. Each user must have their own Kagi account and supply their own credential. No credential is included, requested in chat, or provisioned by installation.
- Obtain a session link from your Kagi browser settings.
- Save it, or just its token, in a private local file using your editor.
- Restrict that file to your user (
chmod 600 /path/to/your/token-file). - Set
KAGI_TOKEN_FILEto that path before starting Pi. The default is~/.secrets/kagi_token.
Never paste a session link or token into a prompt, issue, shell command argument, repository, or Pi settings. No paid Search API key is needed or accepted as a substitute. Authentication failures, challenges and rate limits stop requests; there is no browser bypass or account-login automation. This HTML integration is unofficial and can break when Kagi changes its pages. Use it in accordance with Kagi's terms and your subscription.
Search results are bounded and treated as untrusted source text. A five-minute
in-memory cache reduces repeated queries. Up to four searches run at the same
time. Request starts are at least 150 ms apart, with at most 30 page requests a
minute. So a batch of searches is fast, but a looping agent cannot hammer the
account. A search
that would have to wait past its deadline for that pace fails with a pacing
error instead. The client uses a cooperative I/O deadline;
synchronous parsing cannot be interrupted by that timer. There is a 2 MB
response cap, but pathological HTML can still occupy the event loop.
Without a configured token, the rest of the package loads normally; invoking
kagi_search reports a credential error.
npm ci --ignore-scripts
npm run typecheck
npm test
npm run test:install
npm run audit:packageThe image above is rendered from a real Pi session, not drawn: npm run preview:render stages one against a scripted local endpoint (no model calls)
with a spoken dictation, then lays the captured terminal onto the card. It needs
macOS, tmux, ffmpeg and a provisioned voice model. npm run preview:check -- --open shows it as a 4:3 crop and at README width for review. Only the WebP is
committed; the PNG for GitHub's social preview is written next to it,
ignored by git, and uploaded now and then rather than with each release (see
CONTRIBUTING.md).
Tests use synthetic credentials and isolated homes, not live accounts. The test
runner bounds each suite to two minutes, or ten minutes when CI is set or
with PI_TEST_SERIAL=1. The installation smoke test uses Pi's
real package manager and an isolated home; it never starts a model request.
No build step or install lifecycle hook is needed. Pi loads TypeScript directly.
Runtime dependencies are installed by Pi; no global extension npm install is needed.
The default branch is release-ready: merged commits are updates for unpinned users. See contributing and third-party notices.
