@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.

Packages

Package details

extension

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

  1. 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.
  2. On startup, if pi-ide.autoconnect is not false and exactly one valid lockfile matches cwd, the extension connects automatically. A sticky reconnect target is remembered across /new sessions, keeping the same IDE when multiple editors are open. Use /ide to manually connect, switch IDEs, or disconnect.
  3. Extension opens ws://127.0.0.1:<port>/ with header x-pi-ide-authorization: <authToken> and runs MCP initialize.
  4. While connected:
    • Editor pushes selection_changed notifications. Extension caches filePath, cursor, selection. Renders a status widget.
    • Before each agent run, cached state is injected into the system prompt as an <editor> block.
    • On every write or edit tool call, extension calls openDiff. The call blocks until the editor returns FILE_SAVED (accept) or DIFF_REJECTED (reject). On reject, the tool call is blocked and the rejection is surfaced to the agent.
  5. 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

  1. pi-ide.autoconnect is not false
  2. 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:

  1. Serve MCP on loopback. Write the lockfile on start, remove on shutdown.
  2. Authenticate the upgrade header against the lockfile's authToken.
  3. Implement initialize, tools/list, tools/call.
  4. Implement the tools below.
  5. 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.