Pi Coding Agent
Cursor-like chat sidebar for your local Pi coding agent.
The extension loads your installed Pi SDK directly in the VS Code extension host. It shares
~/.pi/agent (auth, models, packages, sessions, skills, and extensions) and renders the whole agent loop — streaming
replies, tool cards, diffs, terminal output, model and thinking controls, slash
commands, todos, and session history — inside a VS Code / Cursor webview.
Contents
Requirements
- VS Code or Cursor
1.90+
- Local Pi:
npm i -g @earendil-works/pi-coding-agent or bun i -g @earendil-works/pi-coding-agent
- A provider configured in
~/.pi/agent, or use the extension's Accounts page to sign in
Install
If the pi CLI is missing, the chat shows an install card instead of an
error: it detects macOS, Linux, WSL, or Windows, picks bun when you already
have it and npm otherwise, prints the exact command, and runs it in a terminal
so you can watch it. Press Retry afterwards, or point piChat.piPath at a
binary you installed yourself.
Once Pi is installed but no provider is signed in, the chat shows a connect a
provider card with a button straight to the Accounts page — API key, browser
sign-in, and device-code flows all run through your local Pi install. It clears
itself the moment a credential exists, including one supplied by an environment
variable.
The extension activates with the editor and starts Pi in the background, so the
chat is ready the first time you open it rather than spending a few seconds
booting. Set piChat.startOnLaunch to false to start it on first use instead.
From a packaged build:
npm run package
code --install-extension pi-agent-vscode-<version>.vsix # or: cursor --install-extension ...
Reload the window afterwards — a running window keeps the previous build loaded.
Open the chat from the Pi icon in the activity bar, the command palette
(Pi Coding Agent: Open), or ctrl+alt+i.
The chat
How a turn is laid out
One agent run is one chat bubble, and inside it prose and tool calls appear in
the order the model produced them:
I'll read the file first.
┌ read src/greet.ts 3 lines ┐
Found it — the helper ignores the excited flag.
┌ edit src/greet.ts +5 -2 ┐
┌ bash npm test 4.2s ┐
Tests pass.
Prose written after a tool call renders below that tool's card rather than being
glued onto the sentence before it. Consecutive calls share one stack, but each
call always keeps its own card.
Assistant messages show reasoning in a collapsible Thought section (hidden
entirely when Pi's hideThinkingBlock is on), a "Worked for 12s" footer, and a
copy button. Answers longer than ~4,000 characters fold to their tail with a
Show full response button so one long reply cannot bury the rest of the
conversation.
Every tool call renders as its own card labelled with the exact name Pi
registered — bash stays bash, hypa_read stays hypa_read. Cards are typed
by what the tool does:
| Kind |
Rendering |
| diff |
Cursor-style unified diff with per-side gutters and +n -n stats |
| terminal |
Terminal body with the command echoed as $ … |
| file |
Syntax-highlighted contents with line numbers |
| search |
One row per hit, each opening the file at that line |
| todo |
Compact one-liner; the live list is the Todos panel |
Status is honest about what happened: running cards spin, failures show failed
and auto-expand once, and a call still running when the turn ends (abort, crash,
or a reloaded transcript with no persisted result) is marked interrupted
instead of spinning forever. Calls that took over a second show their duration.
Code, diffs, and terminal output
Syntax colours come from your theme. Every token colour resolves from VS Code
theme variables (symbolIcon.*, debugTokenExpression.*, falling back to
charts.*), so code matches whatever theme you use, light or dark. Markdown
fences, file cards, and diff rows all share one palette. Fences that declare a
language highlight while they stream; auto-detection runs once the reply settles.
Terminal output renders like a terminal. ANSI colours map to your terminal
palette (terminal.ansi*), including bright variants, 256-colour, and truecolour,
plus bold, dim, italic, underline, and inverse. Carriage-return progress lines
collapse to their final frame instead of unrolling into hundreds of rows, and
erase sequences clear the line the way a real shell does.
Navigating to code
Everything that names code is clickable:
- File paths in tool card headers open that file —
edit results land on the
first changed line.
- Diff rows open the file at that specific line.
- Search hits open the file at the matching line.
- Inline code in replies links too: a path (
src/pages/CardGallery.tsx, with
optional :42) opens the file, and an identifier (handleConfirmDelete,
CardGallery, renderPaginationItems()) jumps to its declaration through
VS Code's symbol index, with a picker when the name is ambiguous.
Single lowercase words in backticks stay plain text — a link that goes nowhere is
worse than no link.
Message actions
Hover any message:
- User — edit and resend (drops the prompt back in the composer), resend
as-is, copy.
- Assistant — regenerate (re-sends the prompt that produced it), copy.
Context, tokens, and cost
The composer toolbar shows a live meter fed by the usage Pi reports on every turn:
a fill bar with the percentage of the model's context window, and session cost
when the provider prices tokens. It turns amber at 75% and red at 90%, and
clicking it compacts the conversation. While Pi compacts, it says so; prompts
queued behind a running turn show as ↑n.
The composer
Mentions and context sources
Type @ to search the workspace. The index covers the whole tree (build
output excluded) and searching happens on the host, so a deep file like
src/pages/Loyalty/_components/CardGallery.tsx is found by typing CardGallery,
cardgallery, or even cardgal — matching is case-insensitive with subsequence
support, filename hits ranked first.
The same picker offers live editor context:
| Source |
Inserts |
@problems |
Errors and warnings across the workspace, file:line |
@terminal |
The last 80 lines of the active terminal |
@git |
Working tree status and diff stat |
Drag files or folders in, or use the paperclip. Paths with spaces are quoted
automatically, and a file you already typed as @path is never re-sent as an
attachment chip.
Slash commands
Type / for Pi's commands, each showing its argument shape (/fork <entry>)
inferred from its description. Editor context is not injected into slash
commands — they are directives to Pi, not questions about the open file.
Attachments
Mentions render as pills inside the prompt, where you wrote them — picking a
suggestion, dropping a file, or using the paperclip inserts a pill at the caret
rather than stacking chips in a row above the input. A pill shows the filename with an icon for its type — the React mark for
.tsx/.jsx, a TS or JS badge, CSS, npm for package.json, Docker, images,
folders, and so on — carries the full path in its tooltip, and behaves as one
character for selection and backspace. Each pill also has a ✕ to remove it
with the mouse, which tidies the surrounding spaces as it goes.
The text Pi receives is unchanged: each pill serialises back to @path (quoted
when the path has spaces), so drafts, prompt history, and @ search all keep
working on plain strings.
Drafts and history
An unsent prompt (text and chips) is saved per chat, so it survives switching
chats, hiding the panel, and VS Code tearing the webview down. ↑ walks previous
prompts shell-style, most recent first, from an empty box.
You can type while Pi is still launching — the message is queued and sent once it
is ready.
Sessions
The header holds new chat, rename, fork, clone, export, and history. History is
grouped Today / Yesterday / Previous 7 days / Older, searchable, and each row can
be deleted (moved to trash, with confirmation). Pi's direct .jsonl layout and
legacy nested session directories are both supported.
Keyboard shortcuts
Most defaults live in the ctrl+alt block, which keymaps rarely touch, and are
identical on macOS so a config shared across machines behaves the same. The two
exceptions are the model list and thinking levels, which sit on cmd+/ and
option+b where they are quickest to reach.
| Shortcut |
Action |
ctrl+alt+i |
Focus the chat input (opens the view first) |
cmd+/ (ctrl+/) |
Open the model list (outside the editor) |
alt+b (option+b) |
Open thinking levels |
cmd+ctrl+l (macOS)
ctrl+alt+l (Windows/Linux) |
Add the editor selection to Pi |
ctrl+alt+n |
New chat |
ctrl+alt+h |
Chat history |
ctrl+alt+. |
Stop the current run |
ctrl+alt+, |
Settings |
cmd+/ is scoped to !editorTextFocus so VS Code's Toggle Line Comment
keeps working while you are in a file. Anywhere else — the chat, the sidebar,
the panel — it opens the model list.
Every command is contributed to VS Code, so all of it is rebindable: run
Pi Coding Agent: Keyboard Shortcuts (or open keybindings.json) and bind
whatever you like — including chords, e.g.
{ "key": "ctrl+p m", "command": "piChat.openModelPicker" },
{ "key": "ctrl+p n", "command": "piChat.newSession" }
To take cmd+/ everywhere, including inside a file (commenting then moves to
cmd+shift+7):
{ "key": "cmd+/", "command": "-piChat.openModelPicker", "when": "!editorTextFocus" },
{ "key": "cmd+/", "command": "piChat.openModelPicker" }
Inside the chat
ctrl+j / ctrl+k move through every list — slash commands, file mentions, the
model picker, thinking levels, and chat history — alongside the arrow keys,
matching vim-style keymaps that rebind list navigation globally. With no palette
open they step through your prompt history.
| Key |
Behaviour |
enter |
Send |
shift+enter |
Newline |
cmd/ctrl+enter |
Send even with a palette open |
↑ / ctrl+k |
Previous prompt |
↓ / ctrl+j |
Next prompt |
tab |
Accept the highlighted suggestion |
esc |
Close a palette, or stop the run |
backspace |
On an empty input, remove the last file chip |
Editor integration
When code is selected, a native Add to Pi action appears immediately above it.
Click it—or use the editor context menu, title bar action, or cmd+ctrl+l
(ctrl+alt+l on Windows/Linux)—to attach a pill referencing the file,
language, and line range to your next message. Press it again over another
selection to stack on a second pill; each shows up again in the sent message
so you can see exactly what Pi was pointed at.
Commands
| Command |
Notes |
| Open |
Reveal the chat |
| Focus Chat Input |
Reveal and focus the composer |
| New Session / Abort / Compact Conversation |
|
| Add to Pi |
Editor selection → prompt |
| Open Model List / Select Model |
Inline picker / native quick pick |
| Open Thinking Levels / Cycle Thinking Level |
|
| Cycle Model Forward / Backward |
|
| Chat History |
|
| Rename / Export / Clone / Fork Current Session |
|
| Open Settings / Login to Provider / Logout of Provider |
|
| Select Workspace Folder |
Multi-root workspaces |
| Refresh Models and Commands / Restart Pi |
|
| Keyboard Shortcuts |
Opens keybindings filtered to piChat. |
Select Model opens a native quick pick showing each model's context window,
input price, and image support, with a check on the current one.
Settings
| Setting |
Description |
piChat.piPath |
Path to pi (empty = auto: PATH, then ~/.bun/bin/pi) |
piChat.extraArgs |
Extra CLI args after --mode rpc |
piChat.streamingBehavior |
steer or followUp when sending mid-stream |
piChat.injectEditorContext |
Prepend active file/selection after the first prompt |
piChat.toolOutputCapKb |
Cap tool output forwarded to the UI (default 64) |
Settings and accounts page
Open Pi Coding Agent: Open Settings, press ctrl+alt+,, or use the gear in
the chat header. The page manages:
- every
piChat.* extension setting at user or workspace scope;
- common Pi defaults, delivery, transport, compaction, retry, display, and telemetry settings;
- global (
~/.pi/agent/settings.json) and project (.pi/settings.json) model scopes;
- provider login and logout, including API-key, browser OAuth, and device-code flows.
Authentication runs through the installed Pi runtime, so credentials stay in Pi's
normal auth store and provider-specific flows remain authoritative. The webview
never persists secret prompt values. Sign-out removes credentials stored in Pi;
credentials supplied by environment variables or external configuration are
identified separately and must be removed at their source.
Scoped models are saved as Pi enabledModels. Pi uses them for model cycling
after the RPC process restarts; the normal model picker still allows direct
access to all available models.
Development
npm install
npm run typecheck
npm run test:unit # unit + RPC integration tests
npm run test:e2e # browser test against the webview preview harness
npm run lint
npm run build
npm run package # → pi-agent-vscode-<version>.vsix
Press F5 ("Run Pi Coding Agent Extension") for an Extension Development Host,
then open the Pi activity-bar icon.
For UI work without an editor host:
npx vite --config webview/vite.config.ts
webview/preview.html mounts the webview against a mock state covering every
tool card variant, streaming, todos, and the context meter.
Architecture
Pi SDK (in the extension host) ──events──▶ EventReducer src/pi/eventReducer.ts
│ UiPatch[]
▼
PiSessionManager src/pi/sessionManager.ts
│ batched patches (~16ms)
▼
PiChatViewProvider src/bridge/viewProvider.ts
│ postMessage
▼
React webview webview/src
src/pi/sdkClient.ts dynamically loads the SDK beside the configured Pi executable, creates an in-process AgentSessionRuntime, and bridges extension dialogs to the webview without JSONL RPC.
src/pi/eventReducer.ts translates Pi SDK events into UI patches. It
keeps one bubble per turn, tracks prose segments so text lands in the right
place relative to tool cards, and resolves tool identity when events omit ids.
src/bridge/uiState.ts owns the merge policy and is shared by host and
webview, so both apply patches identically. Streaming deltas own a prose
segment while it streams — providers that send the finished text before
replaying it as deltas cannot double the reply.
src/bridge/protocol.ts is the single source of truth for both message
directions.
webview/src renders; it holds no agent logic of its own.
Tests live in test/ and cover the reducer, the bridge, tool display rules, ANSI
rendering, composer text rules, and the file index.