@ldelossa/pi-ide
Pi extension that connects to a running editor (Neovim today, more later) over a local WebSocket. Adds ambient editor context to the agent and routes write/edit tool calls through interactive diffs in the editor.
Package details
Install @ldelossa/pi-ide from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@ldelossa/pi-ide- Package
@ldelossa/pi-ide- Version
0.2.4- Published
- Jun 4, 2026
- Downloads
- 439/mo · 439/wk
- Author
- ldelossa
- License
- MIT
- Types
- extension
- Size
- 43.8 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-ide
Connects a Pi coding-agent session to a running editor over a local
WebSocket MCP server. Provides ambient editor context (current file,
cursor, selection), routes write/edit tool calls through the editor
as interactive diffs, and serves inline code completions (often called
"suggestions") to editors that request them. The wireformat is compatible
with Claude Code's editor integration.
Install
pi install npm:@ldelossa/pi-ide
That writes to ~/.pi/agent/settings.json and loads pi-ide on every pi
session. To scope it to one project instead of globally, add -l:
pi install -l npm:@ldelossa/pi-ide
Project installs land in ./.pi/settings.json and can be checked in so
teammates pick the extension up automatically.
To try it without installing permanently:
pi -e npm:@ldelossa/pi-ide
To remove:
pi remove npm:@ldelossa/pi-ide
Git installs also work if you prefer pinning to a tag:
pi install git:github.com/ldelossa/pi-ide@v0.1.0
You also need an editor that speaks the protocol. The reference Neovim implementation is at https://github.com/ldelossa/pi-ide.nvim.
Once both are running, use /ide inside pi to connect to the editor.
Architecture
- Editor starts a loopback MCP server on a free TCP port. Writes a lockfile to
$PI_IDE_LOCK_DIR(default~/.pi/ide/<port>.lock). Removes it on exit. - On startup, if
pi-ide.autoconnectis notfalseand exactly one valid lockfile matchescwd, the extension connects automatically. A sticky reconnect target is remembered across/newsessions, keeping the same IDE when multiple editors are open. Use/ideto manually connect, switch IDEs, or disconnect. - Extension opens
ws://127.0.0.1:<port>/with headerx-pi-ide-authorization: <authToken>and runs MCPinitialize. - While connected:
- Editor pushes
selection_changednotifications. Extension cachesfilePath, cursor, selection. Renders a status widget. - Before each agent run, cached state is injected into the system prompt as
an
<editor>block. - On every
writeoredittool call, extension callsopenDiff. The call blocks until the editor returnsFILE_SAVED(accept) orDIFF_REJECTED(reject). On reject, the tool call is blocked and the rejection is surfaced to the agent.
- Editor pushes
- Session shutdown closes the socket.
Autoconnect
Autoconnect saves a manual /ide step by connecting to the IDE automatically.
It is enabled by default.
Connections are made on two occasions:
On start
pi-ide.autoconnectis notfalse- Exactly one valid IDE lockfile exists for the current working directory
On session replacement (/new, /resume, /reload)
If the previous session was connected to an IDE, pi-ide tries to reconnect
to that same IDE. This sticky reconnect bypasses the single-candidate rule,
which means it can reconnect even when a second IDE has since started in the
same directory. If the sticky IDE is no longer valid (closed, stopped,
connection fails, or its project changed), autoconnect falls back to the
cold-start rule.
Disabling
Add to any Pi settings file:
{
"pi-ide": {
"autoconnect": false
}
}
| Scope | File |
|---|---|
| Global | ~/.pi/agent/settings.json |
| Project | .pi/settings.json |
Project settings override global settings. A missing setting is treated as enabled.
Lockfile
Path: $PI_IDE_LOCK_DIR/<port>.lock (default ~/.pi/ide/<port>.lock).
{
"pid": <int>,
"workspaceFolders": ["<abs path>", ...],
"ideName": "<display name>",
"transport": "ws",
"authToken": "<uuid>"
}
Transport
- WebSocket on
127.0.0.1:<port>, path/. - Required header:
x-pi-ide-authorization: <authToken>. Mismatches must be rejected before upgrade. - Framing: JSON-RPC 2.0 in text frames. One message per frame.
- Protocol: MCP
2024-11-05. Subset only.resources/*,prompts/*,logging/*, and sampling are not used. Server-reported capabilities are ignored by the client. Four methods carry the protocol:initialize,notifications/initialized,tools/list,tools/call.
Editor-side contract
The editor must:
- Serve MCP on loopback. Write the lockfile on start, remove on shutdown.
- Authenticate the upgrade header against the lockfile's
authToken. - Implement
initialize,tools/list,tools/call. - Implement the tools below.
- Emit the notifications below.
Tool: openDiff
Input:
| field | type | description |
|---|---|---|
old_file_path |
string | absolute path of the existing file |
new_file_path |
string | destination path (typically same as old) |
new_file_contents |
string | proposed contents |
tab_name |
string | stable id for this diff invocation |
Behavior: open a two-pane diff between on-disk content and proposed content. Block (suspend the JSON-RPC response) until the user accepts or rejects.
Response on accept:
{ "content": [
{ "type": "text", "text": "FILE_SAVED" },
{ "type": "text", "text": "<final contents>" }
]}
Final contents may differ from new_file_contents if the user edited the diff
before saving.
Response on reject:
{ "content": [
{ "type": "text", "text": "DIFF_REJECTED" },
{ "type": "text", "text": "<tab_name>" }
]}
Tool: close_tab
Input: tab_name (string). Close the named diff if open. No-op otherwise.
Response:
{ "content": [ { "type": "text", "text": "TAB_CLOSED" } ] }
Notification: selection_changed
JSON-RPC notification (no id). Sent on cursor move, mode change, buffer
enter, and text change. Debounce locally (~100ms recommended).
{
"text": "<selected text, empty when no selection>",
"filePath": "<abs path>",
"fileUrl": "file://<abs path>",
"selection": {
"start": { "line": <0-based>, "character": <0-based> },
"end": { "line": <0-based>, "character": <0-based> },
"isEmpty": <bool>
}
}
Lines and characters are zero-based. The extension renders line numbers as 1-based.
Editor-initiated requests
The editor may send JSON-RPC requests to the extension over the same WebSocket. The extension processes them and returns responses.
Request: getSuggestions
Returns inline code completions for the editor to render as ghost text or
equivalent. The extension calls the configured model (current session
model by default; see model precedence below), parses up to 3
<SUGGESTION>...</SUGGESTION> blocks from the response, and returns them.
Input:
| field | type | description |
|---|---|---|
filePath |
string | optional. absolute path of the cursor file |
language |
string | optional. filetype or language id |
outline |
string | optional. structural sketch of the file |
enclosingScope |
string | optional. surrounding function or class |
cursorBefore |
string | text before cursor (typically ~20 lines) |
cursorAfter |
string | text after cursor (typically ~10 lines) |
suggestionCount |
int | optional. cap on returned alternatives. Max 3. |
model |
string | optional. preferred model "provider/id". CLI flag wins if set. |
outline is whatever structural sketch the editor produces with its native
source-analysis tool — treesitter sexpr (Neovim), document symbols from
LSP (VS Code), PSI tree (JetBrains), or omitted entirely. The model accepts
any text. Completion quality scales with the amount of structural context
provided. The minimal viable payload is cursorBefore and cursorAfter.
Response:
{ "suggestions": ["<text>", ...] }
Empty suggestions is valid and means the model declined to complete.
Cancellation: editor sends request_cancelled notification with
{ "id": <request id> }. The extension aborts the in-flight model call.
Model precedence: the CLI flag (--pi-ide-suggestion-model <provider>/<id>)
wins. Otherwise the editor-provided model field is used. If neither is
set, the current session's model is used.
To find valid provider/id strings, run pi --list-models and join the
provider and model columns with a / (e.g., openai/gpt-4o).
For suggestions, prefer low-latency models. Large reasoning models often
feel too slow for inline completion; smaller coding-capable models such as
openai-codex/gpt-5.4-mini or deepseek/deepseek-v4-flash usually provide
a better interactive experience.
Request: listSuggestionModels
Returns models available to the current pi session for runtime suggestion model selection in editors. The extension is the source of truth for model discovery; editors should not read pi config files directly.
Input: none.
Response:
{
"cliOverride": "<provider/id>", // optional. --pi-ide-suggestion-model is active
"currentModel": "<provider/id>", // optional. current pi session model
"models": [
{ "provider": "openai", "id": "gpt-4o", "name": "GPT-4o", "model": "openai/gpt-4o" }
]
}
If cliOverride is present, editor-provided model selections are still
accepted by the editor but will not affect suggestions because the CLI flag
has higher precedence.
Reference editor implementation
Neovim: pi-ide.nvim. Implements both the editor-side contract (diffs,
diagnostics, selection notifications) and the getSuggestions client.
Suggestions always work; treesitter and LSP enrich the context but are
not required — the feature degrades gracefully to cursor-window-only
context when either is unavailable.