Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pi-bridge.nvim

Talk to pi from Neovim. Hit <leader>ai, type your question with placeholders like @this or @selection to include code context — pi streams its response in the TUI.

No copy-pasting. No context switching. Just ask.

:PiBridge fix @this              " send current line
:PiBridge explain @selection     " send visual selection
:PiBridge fix these @diagnostics " send LSP errors
:PiBridge review @buffer         " send entire file

pi auto-launches in a split if it's not running. Buffers auto-refresh from disk when the agent finishes. Works per-project — each directory gets its own pi instance.

Pairs with pi-bridge.ext (the pi extension that receives messages).

Install

Requires pi-bridge.ext installed in pi (see pi-bridge.ext install).

lazy.nvim

Pin to releases (recommended):

{
  "junkfactory/pi-bridge.nvim",
  version = "*",  -- latest tagged release
  cmd = "PiBridge",
  keys = {
    { "<leader>ai", mode = { "n", "v" }, desc = "Ask pi" },
  },
  opts = {
    -- see Setup section for all options
  },
}

Or track main (may encounter instability):

{
  "junkfactory/pi-bridge.nvim",
  -- no version key; tracks main
  cmd = "PiBridge",
  keys = {
    { "<leader>ai", mode = { "n", "v" }, desc = "Ask pi" },
  },
  opts = {},
}

lazy.nvim automatically calls require("pi-bridge").setup(opts) — no config = function() needed.

Native packages

Pin to a release (recommended):

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone --branch v0.1.0 --depth 1 https://github.com/junkfactory/pi-bridge.nvim.git \
  ~/.local/share/nvim/site/pack/plugins/start/pi-bridge.nvim

Or track main (may encounter instability):

mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/junkfactory/pi-bridge.nvim.git \
  ~/.local/share/nvim/site/pack/plugins/start/pi-bridge.nvim

Restart nvim after cloning. The plugin loads automatically.

From a local clone (development)

Clone the repo and point your plugin manager at it. Edits under lua/ are picked up on the next :source or nvim restart.

lazy.nvim — use dir so lazy loads the local checkout instead of fetching from GitHub:

{
  dir = "/absolute/path/to/pi-bridge.nvim",
  name = "pi-bridge.nvim",  -- preserve the plugin name for lazy's bookkeeping
  opts = {
    -- see Setup section for all options
  },
}

Run :Lazy install once to register; subsequent edits in the clone take effect after restarting nvim or running :Lazy reload pi-bridge.nvim.

Native packages (no plugin manager) — symlink the clone into pack/local/start so Neovim auto-loads it on startup:

mkdir -p ~/.local/share/nvim/site/pack/local/start
ln -s /absolute/path/to/pi-bridge.nvim \
      ~/.local/share/nvim/site/pack/local/start/pi-bridge.nvim

Restart nvim after editing files under lua/, plugin/, or doc/ so Neovim re-scans the runtimepath. The symlink stays in sync with your working tree automatically.

Releasing

Each repo (pi-bridge.nvim, pi-bridge.ext) is released independently. The exception is a socket protocol change — both repos are then tagged at the same version.

Releases are triggered by tagging. The tag.sh script handles validation, build checks, tagging, and pushing:

./.github/ci/tag.sh 0.1.2   # no 'v' prefix — script adds it

This runs the full test suite (make test), creates a v0.1.2 jj tag on main, and pushes. The push triggers a CI job that creates the GitHub release with auto-generated notes.

Cross-repo pairing

After both releases exist, a daily CI job appends a pairing line (e.g. "Requires pi-bridge.ext v0.1.2") to each release's notes.

Dry run

DRY_RUN=1 ./.github/ci/tag.sh 0.1.2

Runs checks and prints the tag/push commands without mutating anything.

Version pinning

Prefer version = "*" while pre-1.0 — semver guarantees are soft before 1.0, so tracking the latest tag is safer than pinning.

Usage

Mode Mapping Action
Normal <leader>ai Prompt for a message
Visual <leader>ai Prompt with selection as context

Or use the command:

:PiBridge              " prompt for message
:PiBridge hello world  " send message directly

All responses render in pi's TUI — streaming text, tool calls, diffs, etc.

Placeholders

Include context directly in your message using placeholders:

Placeholder Replaces with Example
@this Current line as a fenced code block (filetype language) ```lua local x = 1 ```
@selection Visual selection as a fenced code block (empty in normal mode) Fenced selection text
@buffer Absolute path to current buffer /path/to/file.lua
@buffers Newline-separated list of open buffer paths /path/a.lua\n/path/b.lua
@content Buffer content as a fenced code block (truncated at ~900KB if large) Full buffer contents
@diagnostics LSP diagnostics for current buffer L1:C1 [ERROR] unused variable
@marks One block per set letter mark (A-Z global + a-z buffer-local; digits excluded as auto-managed; literal @marks when none) One block per mark
@mN Block for mark N (A-Z global; a-z, 0-9 buffer-local; literal when unset) Block for mark A

Examples:

:PiBridge fix @this
:PiBridge explain @selection
:PiBridge fix these @diagnostics
:PiBridge refactor @this and check @diagnostics
:PiBridge review @buffer
:PiBridge summarize @content
:PiBridge compare @buffers
:PiBridge explain @mA

Unknown @tokens pass through unchanged. Typing @ in the prompt shows autocomplete suggestions.

@this and @selection also report their line range to pi, which appears on the File: header link as File: [main.lua:12-200](/path/to/main.lua). @content is not ranged.

Each mark renders as a single line: a Vim mark X - [basename:LINE](/abs/path): header mirroring the File: link shape (clickable in pi's TUI), followed by the mark's line as an inline code span — truncated at 200 characters, since marks are context rather than code to edit. @marks covers letter marks across all listed buffers (globals A-Z first, then each buffer's locals); every mark it lists can also be addressed individually with @mN. @mN with an unset mark, and @marks with no marks set, stay literal. Mark placeholders do not report a line range.

Setup

require("pi-bridge").setup({
  -- Split direction: "vertical" or "horizontal"
  split_direction = "vertical",

  -- Split size (percentage or absolute lines/cols)
  -- nil = use Neovim defaults (50%)
  split_size = nil,  -- e.g., 80 for 80 cols vertical, 20 for 20 lines horizontal

  -- Auto-launch pi if socket missing/invalid
  auto_launch = true,

  -- Max seconds to wait for socket after launching pi
  launch_timeout = 10,

  -- Command to launch pi (allows custom args, env, etc.)
  launch_cmd = { "pi" },

  -- Keymaps (set to false to disable default keymap)
  keymaps = {
    prompt = "<leader>ai",  -- normal + visual mode
  },

  -- Log level: "trace" | "debug" | "info" | "warn" | "error"
  log_level = "info",

  -- Edit approval prompt: when pi is about to edit/write a file,
  -- Neovim shows a y/a/n picker (vim.ui.select) — the diff itself
  -- renders in pi's TUI. Set false to opt out (pi falls back to its
  -- own TUI overlay because no approval_ack arrives within 1s).
  edit_approval_prompt = true,

  -- UI prompt mirror: mirror any pi extension's blocking prompt
  -- (select / confirm / custom `ctx.ui.custom` components) into
  -- Neovim during nvim-originated turns. Set false to opt out (the
  -- ext side keeps wrapping, but nvim never sends mirror_ready so
  -- every prompt stays in pi's TUI). See pi-bridge.ext's
  -- PI_BRIDGE_UI_PROMPT_MIRROR env var for the other direction.
  ui_prompt_mirror = true,

  -- Send/receive notifications: show a toast when pi starts/finishes
  -- working. Set false to silence (on base Neovim toasts require
  -- Enter to dismiss). Errors and disconnect warnings are always shown.
  notify = true,
})

Keymaps

Remap or disable:

-- Custom key
keymaps = { prompt = "<leader>p" },

-- Disable defaults (use :PiBridge command)
keymaps = false,

How It Works

┌─────────────┐     Unix Socket     ┌─────────────────┐
│  pi (TUI)   │◄──────────────────► │ pi-bridge.nvim  │
│  + extension│     JSON msgs       │ (Lua)           │
└─────────────┘                     └─────────────────┘

Socket Discovery

On <leader>ai:

  1. Get cwd from vim.fn.getcwd()
  2. Walk from cwd upward toward $HOME, probing each directory for an active socket
  3. The socket path is ~/.pi/agent/pi-bridge/sockets/<sha256(dir)>.sock — same hash algorithm as pi-bridge.ext
  4. On no hit → auto-launch pi (if enabled)

The upward walk means opening Neovim in src/foo/bar/ finds the pi instance running at the project root without requiring :cd first. The walk stops at $HOME and never probes filesystem root.

Auto-Launch

When the socket doesn't exist or connection fails:

  1. Open a split (configured direction)
  2. Run pi in a terminal buffer
  3. Wait for socket to appear (polls every 200ms up to launch_timeout)
  4. Connect and send message
Scenario Behavior
Socket exists but connection refused Relaunch
User closes pi split manually Next prompt detects missing socket, relaunches
Pi exits unexpectedly Socket disappears, next prompt triggers relaunch
auto_launch = false Error: "pi-bridge socket not found. Launch pi manually."

Startup Checks

On setup(), warns if vim.o.autochdir is enabled — socket matching uses cwd, which autochdir changes per-file.

Cwd Contract

Sockets are keyed by sha256(cwd). Each project directory gets its own socket — there is no global singleton and no shared registry. The resolver walks from your current directory upward to $HOME to find the nearest active socket, which makes nested buffers work without configuration.

autochdir is not supported. Because autochdir changes cwd per-buffer, the socket you reach depends on which file is active rather than which project you intended. The plugin warns on setup() if it is enabled and :checkhealth pi-bridge flags it under a dedicated autochdir line.

For project switching, use:

  • :cd {path} — change cwd for the whole window
  • :lcd {path} — change cwd for the current window only

Both work normally. Avoid autochdir and avoid changing cwd with autocommands that fire on every buffer change.

:checkhealth pi-bridge states

Health distinguishes between "is there a server" and "is Neovim connected to it". Two different things, two different reports:

Output Meaning
OK Socket: connected Persistent Neovim connection is up. Sending prompts will work.
INFO Socket available, Neovim not connected: <path> pi is running but Neovim hasn't connected yet. Run :PiBridge to connect.
WARN Socket file present but unreachable: <path> (<reason>) Stale socket file from a previous crash. Remove it or relaunch pi.
INFO Socket: not connected (no socket file in cwd) No server in this cwd. Auto-launch will start one if enabled.

:checkhealth only inspects state; it never opens or closes the persistent connection.

Edit Approval Prompt

When pi is about to apply its edit or write tool for a request that originated from Neovim, it sends an approval_request over the socket. Both surfaces then offer the same choice — Neovim's picker and a y / a / n prompt that replaces pi's input box — and whichever answers first wins. When pi answers (or the request settles for any reason), it broadcasts approval_resolved and the Neovim picker is dismissed automatically — no stale prompt waiting on an already-answered request. (Rationale: two answering surfaces with a lingering picker confused users; dismissal removes that.)

Edits you type directly into pi never prompt — only Neovim-sent requests are gated.

The picker follows your configured vim.ui.select (dressing, snacks, fzf-lua, ...). With the stock picker — which blocks in inputlist() and cannot be closed programmatically — pi-bridge shows its own minimal floating prompt (same choices, same keys) so dismissal still works.

Choice Decision Effect
y yes Approve this single tool call
a all Approve all future edits to that file this session
n no Reject the tool call (pi narrates the rejection)
<Esc> no Dismissed picker — same as n

An approval_ack is sent the moment the request arrives (kept for compatibility with older pi versions; current pi no longer requires it — there is no ack timeout).

If pi disconnects while the picker is open, the picker is dismissed with a pi disconnected message and no response is sent.

If the buffer for the target file is loaded and &modified, the prompt notes it — the diff is always computed from disk, so what you see is what pi will apply.

Disabling

require("pi-bridge").setup({ edit_approval_prompt = false })

With this set, nvim never opens the picker or acks; pi's own prompt becomes the sole decision surface for Neovim-sent edits.

Protocol pairing

approval_request, approval_resolved, approval_ack, and approval_response are new NDJSON message types. Both pi-bridge.nvim and pi-bridge.ext must be tagged at the same version when this protocol is in use — see Releasing for the paired-tag rule.

UI Prompt Mirror

When a turn is started from Neovim (<leader>ai) and any pi extension raises a blocking prompt (ctx.ui.select / ctx.ui.confirm / a custom ctx.ui.custom component such as pi-permission-system's permission dialog), pi-bridge surfaces it in Neovim. select / confirm are fully answerable from either surface (first answer wins; the other cleans itself up); custom prompts are answered in pi — Neovim shows a passive notice with an Esc-to-abort escape hatch.

Turns typed directly into pi auto-pass-through (no mirror, no stall) — the mirror is origin-scoped exactly like the edit-approval gate.

Surfaces

Kind What appears in Neovim
select vim.ui.select picker — full original labels as items, with a format_item that display-trims labels longer than 80 chars to first 77 chars + "...". The picker returns the full untrimmed label, so the value sent back to pi is never truncated. Plugin pickers (dressing, snacks, fzf-lua, ...) work untouched. The stock vim.ui.select (which blocks in inputlist() and cannot be dismissed programmatically) is replaced by a minimal owned float with numbered keys 1..9 + <Esc>.
confirm Same as select, folded in as a 2-option picker (Yes / No). Cancelled sends cancelled: true and the ext side resolves to false (matches pi's own RPC semantics).
custom A passive, focused, message-only notice float: "π pi needs your input — answer in the pi window". pi owns the dialog and its navigation completely; no dialog content or re-renders are mirrored, and no key is forwarded. The notice honors only <Esc>: it closes the notice and injects an Esc keypress into the pi-side component (ui_prompt_response {id, key = "\x1b"}), so the component's own Esc handler decides what abort means (e.g. deny for pi-permission-system). Any other key is swallowed while the notice is focused. If the dialog resolves in pi first, ui_prompt_resolved closes the notice and breaks the loop.

Handshake

mirror_ready is sent once per successful connect (persistent and post-launch). The ext side installs its wrappers onto ctx.ui only after receiving it; without it, every extension prompt goes straight to pi's TUI.

The ready flag is cleared on socket disconnect (and a new mirror_ready is required on reconnect). On session boundaries (session_start / session_before_switch / session_shutdown) the ext side drops any open mirror surface; nvim simply no-ops on a stale id.

Disconnect

  • nvim disconnects mid-prompt → the pi-side surface stays open awaiting the user's answer; nvim never resolves the pending request
  • pi disconnects mid-mirror → nvim dismisses its picker/float with a pi disconnected message and sends no response (same as the edit-approval flow)

Disabling (ui_prompt_mirror)

require("pi-bridge").setup({ ui_prompt_mirror = false })

With this set, nvim never sends mirror_ready; the ext side's wrappers pass through, and every extension's prompt is answered in pi's TUI. The other direction is the ext env var PI_BRIDGE_UI_PROMPT_MIRROR=0 (see pi-bridge.ext Kill Switches) — both kill switches are independent.

Mirror protocol pairing

mirror_ready, ui_prompt_request, ui_prompt_response, and ui_prompt_resolved are new NDJSON message types. Both pi-bridge.nvim and pi-bridge.ext must be tagged at the same version when this protocol is in use — see Releasing for the paired-tag rule.

Logging

Logs to vim.fn.stdpath("log") .. "/pi-bridge.nvim.log" (resolves to ~/.local/state/nvim/log/pi-bridge.nvim.log on Linux).

  • Socket connection attempts (success/failure, path)
  • Auto-launch triggers
  • Messages sent to pi (prompt + context summary)
  • Events received from pi

Running Tests

make test                  # run all tests
make test-context          # context module only
make test-placeholders     # placeholders module only
make test-init             # init module only
make test-resolve          # socket resolver only
make test-health           # :checkhealth only
make test-approval         # edit approval prompt only

Requires Neovim 0.12.5+ and mini.nvim (auto-fetched as a test dependency).

Related

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages