原始内容
pi-nvim-bridge
Bridge pi's in-prompt completion into the Neovim instance pi launches as
$EDITOR.The installable extension is named
pi-nvim-bridge(what you see inpi list); the companion Neovim plugin ispi-bridge.nvim.
Demo:
https://github.com/user-attachments/assets/12ff8f75-4bf8-4b27-a41b-d001633ea256
This repository ships two components that work together:
pi-nvim-bridge(this README's focus — a pi extension) captures pi's live autocomplete engine and serves it over a local Unix socket to the external editor pi spawns.pi-bridge.nvim(the companion Neovim plugin, shipped in this same repo at the root — see:help pi-bridge) connects to that socket and renders pi's completion inside Neovim:/commands,skill:templates, argument completions,@filereferences, and filesystem paths.
Acceptance is delegated back to pi's applyCompletion, so insertion behavior
is byte-for-byte identical to the TUI. Both components are complete and
shipped.
What it does
It brings pi's autocomplete into Neovim. When you launch your external editor
from pi, it behaves just like pi's prompt box — same / slash commands, same
skill: templates, same @file references and path suggestions, same argument
completions. Whatever pi would suggest inline, you get here, inside Neovim.
The idea is simple: write your prompt in a real editor without giving up any of pi's autocomplete. You get a comfortable editing surface and the full set of completions pi already offers.
Note: the companion
pi-bridge.nvimplugin is complete (its runtime files live at the repo root:lua/pi-bridge/,plugin/pi-bridge.lua,ftplugin/,doc/). The bridge and plugin work together to deliver pi-faithful completion in the external editor.
Prerequisites
- pi with extension support.
- Neovim ≥ 0.11 (0.12 verified) for the companion plugin — the exact-UTF-16
cursor conversion needs the 3-arg
vim.str_utfindexoverload added in 0.11;:checkhealth pi-bridgeenforces the floor. fd(optional) — enables fuzzy@filesearch. Without it@filesilently returns nothing, but path completion (directory listing) still works.- The companion
pi-bridge.nvimplugin (installed via lazy.nvim — see Installation).
Installation
# Preferred: install from git
pi install npm:pi-nvim-bridge
# Or from a local clone
git clone https://github.com/dabstractor/pi-nvim-bridge
cd pi-nvim-bridge
pi install .
Verify:
pi list # should show "pi-nvim-bridge"
⚠️ Multi-file package — no single-file drop-in. This extension is composed of four interdependent
.tsfiles (pi-nvim-bridge.ts,connection.ts,jsonl-reader.ts,protocol.ts). You cannot install it by copying one file into~/.pi/agent/extensions/. The imported siblings would be unresolved. Install it as a pi package via one of the commands above.
Companion plugin (pi-bridge.nvim, same repo) — install with your plugin
manager. lazy.nvim (note lazy = false so the VimEnter startup shim sources
before activation):
{
"dabstractor/pi-nvim-bridge",
lazy = false,
config = function() require("pi-bridge").setup({}) end,
}
The repo's nvim runtime files (lua/pi-bridge/, plugin/pi-bridge.lua,
ftplugin/, doc/) live at the root, so the clone lands directly on
runtimepath — no dir/sub hack needed (and none would be portable:
no Neovim plugin manager supports cloning a subdirectory as the rtp root).
With any other manager, just ensure the repo root is on your &runtimepath.
See :help pi-bridge (doc/pi-bridge.txt).
Configuration ($EDITOR)
Tell pi to use Neovim as its external editor. Any one of:
export EDITOR=nvim
# or
export VISUAL=nvim
…or in pi's settings.json (this takes precedence over the env vars):
{ "externalEditor": "nvim" }
Then in pi, press Ctrl+G (the app.editor.external keybinding) to
open the external editor. The bridge advertises PI_NVIM_BRIDGE to the Neovim
process pi spawns, which the companion plugin keys on.
Optional startup optimization
A heavy daily-driver Neovim config (LSP servers, lazy-loaded plugins, plugin-
manager boot) can add hundreds of milliseconds-to-seconds to every Ctrl+G
launch. You can keep a tiny dedicated config that loads only pi-bridge.nvim
so the external-editor round-trip feels instant. Two ways to opt in:
Recommended — the bridge opt-in (PI_NVIM_APPNAME):
export PI_NVIM_APPNAME=1
The extension sets NVIM_APPNAME inside pi's process for the session (so the
pi-spawned Neovim boots from ~/.config/pi-bridge/ instead of
~/.config/nvim/) and restores your prior value on exit — so a global
NVIM_APPNAME, if you have one, is left untouched. Value table:
PI_NVIM_APPNAME |
resulting NVIM_APPNAME |
|---|---|
| (unset) | (feature off — nothing changes) |
"", 1, true, yes, on (case-insensitive) |
pi-bridge (default) |
| any other non-empty string | that literal appname |
The minimal config (any of the opt-in paths above) — create
~/.config/pi-bridge/init.lua that loads only pi-bridge.nvim (stock Neovim,
no plugin manager required):
-- ~/.config/pi-bridge/init.lua
require("pi-bridge").setup({})
Neovim starts cleanly even before you create this file (it just has no user config), so the opt-in is safe to enable first and configure at your leisure.
⚠️ The appname must be a simple directory name (no
/). Neovim rejects an appname containing a path separator; we don't validate it in the bridge — nvim surfaces a clear error in the launched editor.
Manual alternative: users who prefer not to use the extension opt-in can
export NVIM_APPNAME=pi-bridge in the shell that launches pi — the child editor
inherits it the same way (note: unlike the bridge opt-in, this does not restore
a prior value because pi never owned the var).
How it works
- Live-provider capture. The extension registers a pass-through
AutocompleteProviderFactorywith pi. The factory simply captures thecurrentprovider pi hands it — so the bridge serves the same completion the TUI uses, not a snapshot. - Process-local discovery. pi spawns
$EDITORinheritingprocess.env(noenv:override,stdio: "inherit"). The bridge writes a JSON descriptor toprocess.env.PI_NVIM_BRIDGEinside pi, so the child Neovim sees it. - JSON-RPC over a Unix socket. The bridge speaks newline-delimited JSON-RPC
with these methods:
getSuggestions,applyCompletion, andshouldTriggerFileCompletion, plushello(token handshake),ping,bye, andgetCommands. Completion acceptance calls back into pi's ownapplyCompletion, guaranteeing identical insertion semantics.
The PI_NVIM_BRIDGE environment variable
The descriptor is a single-line JSON object:
{
"transport": "unix",
"path": "/tmp/pi-nvim-bridge-<pid>.sock",
"token": "<32-byte hex>",
"pid": 12345,
"cwd": "/your/project",
"fdAvailable": true,
"serverVersion": "0.1.0"
}
echo $PI_NVIM_BRIDGEshows nothing in your shell — this is expected. The variable is written toprocess.envinside the pi process and is only visible to the child$EDITORpi spawns. It is never exported to your shell. This is the #1 source of confusion; it is not a bug. To inspect it from inside the launched Neovim::lua print(vim.env.PI_NVIM_BRIDGE).
Troubleshooting
- "I typed, then
:q, and lost my prompt." pi only reads the temp file after the editor exits with status 0. The companion plugin autosaves the buffer onVimLeavePrewhen modified (autosave_on_exit, defaulttrue). To be explicit, quit with:x/ZZ. - "Completion doesn't appear in Neovim."
Confirm the extension loaded (
pi listshowspi-nvim-bridge), confirmEDITOR=nvim(orexternalEditorin settings), and confirm the companionpi-bridge.nvimplugin is installed and that it gated itself on the presence ofPI_NVIM_BRIDGE. - "
@filefinds nothing." Installfd. The bridge reportsfdAvailableinhello; withoutfd,@fileis empty but path completion (directory listing) still works. - "I ran
/reloadwhile the editor was open." The bridge re-captures the provider and re-advertises the descriptor; the open editor's existing connection stays valid, and acommandsChangednotification fires so it can refresh. - "Another extension's custom trigger (e.g.
#issues) doesn't complete." Known limitation. The bridge captures the provider at its own registration time, so wrappers registered after it won't appear. The base provider — slash commands,skill:, templates, and paths — is always captured. - "Nothing happens in non-interactive mode (
pi -p)." Correct:openExternalEditoris TUI-only, so the bridge no-ops whenctx.mode !== "tui".
Security
- The Unix socket lives in
os.tmpdir()with0600permissions. - A 32-byte random token prevents another local process from impersonating
the editor. It is delivered via
process.env(process-local, never on disk) and validated in thehellohandshake. - The server rejects any method before a valid
hello. - Never log or echo the token. Treat the
PI_NVIM_BRIDGEdescriptor as sensitive — don't paste it into bug reports or completion descriptions.
Development
This is a packaging-and-docs repository; the extension source lives under
extension/.
# Type-check (portable, reproducible gate)
npm run typecheck
# …or directly:
npx tsc --noEmit -p extension/tsconfig.json
Tests use node:test with jiti (not vitest). Run a single suite:
JITI_REG=/home/dustin/.local/lib/node_modules/@earendil-works/pi-coding-agent/node_modules/jiti/lib/jiti-register.mjs
node --import "$JITI_REG" extension/tests/bridge-env.test.ts
# Run all suites:
for f in extension/tests/*.test.ts; do
node --import "$JITI_REG" "$f" >/dev/null 2>&1 || echo "FAIL: $f"
done
The
JITI_REGpath above is machine-specific (it points at pi's bundled jiti). Adjust it to your own pi install. This is why thetestnpm script is a pointer rather than a hardcoded command.
Repository layout:
pi-nvim-bridge/
├── package.json # pi package manifest (pi.extensions → extension entry)
├── README.md # project README (extension + nvim plugin)
├── LICENSE
├── plugin/pi-bridge.lua # VimEnter auto-activation shim ┐
├── lua/pi-bridge/ # init/bridge/completion/menu/coords/health/ │ nvim plugin
│ # blink_source/cmp_source/notify/jsonlreader │ runtime files
├── ftplugin/pi-prompt.lua # 9 buffer-local keymaps + ft opts + autocmds │ at the REPO ROOT
├── doc/pi-bridge.txt # the vimdoc (`:help pi-bridge`) │ (portable install)
├── tests/ # plenary specs + plenary-free smokes ┘
└── extension/ # the pi-nvim-bridge pi extension (TypeScript)
├── pi-nvim-bridge.ts # entry: default-export factory
├── connection.ts # JSON-RPC server + dispatch + registry
├── jsonl-reader.ts # newline-delimited JSON framing
├── protocol.ts # type-only: descriptor + RPC envelopes
├── tsconfig.json
└── tests/ # node:test + jiti suites
Why runtime files live at the root: every Neovim plugin manager (lazy.nvim, packer, vim-plug, mini.deps) clones the repo and adds its root to
&runtimepath; none portably supports a subdirectory as the rtp root. Thepi-nvim-bridgenpm package is unaffected —package.jsonfilesscopes the published tarball toextension/*.ts+ README + LICENSE, so the lua never enters npm.
LICENSE: the manifest declares
"license": "MIT"and anLICENSEfile is committed at the repo root.
Links
- PRD — full design document for the bridge + Neovim plugin.
- pi docs: packages.md · extensions.md
- Companion plugin:
:help pi-bridge(doc/pi-bridge.txt).
Releasing
Releases are tag-driven. The git tag is the single source of truth for the
published version — you never hand-edit package.json's "version".
git tag v1.2.3
git push origin v1.2.3
Pushing a v* tag triggers .github/workflows/release.yml,
which:
- extracts the version from the tag (strips the leading
v, validates it as semver), - writes that version into
package.jsoninside the ephemeral runner checkout only (npm version … --no-git-tag-version) — nothing is committed back to the repo, - publishes
pi-nvim-bridgeto npm with provenance attestation, and - opens a GitHub Release with auto-generated notes (prereleases are auto-marked
when the version contains a
-, e.g.v1.0.0-beta.1).
One-time setup: add an npm access token (Automation or Granular, with
publish rights for pi-nvim-bridge) as the repo secret NPM_TOKEN.
Provenance also relies on the workflow's id-token: write permission, which is
already declared in the workflow. If your token type can't do provenance, set
publishConfig.provenance to false in package.json (and drop
--provenance from the publish step).
The repo's package.json "version" stays at a placeholder (currently
0.1.0) on purpose — it is always overridden at publish time by the tag.