Skip to content

Repository files navigation

🤖 pi-agent.nvim

Pragmatic integration between Neovim and the pi agent harness.

✨ Features

  • Open pi inside a Neovim window/tab, or in a new terminal multiplexer tab (tmux/herdr)
  • Send context directly from Neovim; cursor/selection locations, selection contents, etc
  • Let your agent read and edit the quickfix list via tools (nvim_get_qflist, nvim_set_qflist)
  • Trigger Lua functions on pi events: pi.on("agent_settled", function() ... end)
  • pi extension bundled with plugin, no extra dependencies, built to be hackable

🔧 Tools

nvim_get_qflist / nvim_set_qflist

Let your agent access and edit the quickfix list.

Try asking:

  • explain everything in my quickfix list
  • review last commit and put issues in my qflist
  • find all instances of exception swallowing and put them in my qflist

📡 Listen to events

Listen to arbitrary pi events in Neovim:

-- .on listens to events asynchronously and does not block the pi event handlerpi.on("agent_settled", function(event) vim.notify("Agent settled!") end)
-- .on_blocking *does* block the pi event handler and uses this return value on the pi sidepi.on_blocking("message_end", function(event)
ifevent.message.role=="user" thenevent.message.content[1].text=event.message.content[1].text.." -- btw: hello from pi-agent.nvim!"returneventendend)

📎 Passing context

You can paste text to your in-progress pi message/prompt in multiple ways:

MethodDescription
paste_cursor_location()paste current cursor location (file:linenum)
paste_selection_location()paste current selection location (file:startline-endline)
paste_selection_contents()paste current selection contents
paste_qflist()paste quickfix list

Quickstart

Install

-- lazy.nvimreturn {
"Run1e/pi-agent.nvim",
config=function()
require("pi-agent").setup()
-- add your keymaps here...end,
}
-- vim.packvim.pack.add({ "https://github.com/Run1e/pi-agent.nvim"})
require("pi-agent").setup()
-- add your keymaps here...

Setup

pi-agent supports three "surfaces" (where your pi instance runs) out of the box: nvim, herdr, and tmux.

If omitted it defaults to opening pi in a Neovim vsplit.

To set a specific surface, for example herdr:

localpi=require("pi-agent")
pi.setup({
surface=pi.get_surface("herdr")
})

All surfaces have their own options you can configure, see surface options

Keymaps

pi-agent ships with no default keymaps.

Here's a list of recommended defaults to get you started:

vim.keymap.set("n", "<leader>as", pi.start, { desc="pi: Start" })
vim.keymap.set("n", "<leader>af", pi.focus, { desc="pi: Focus" })
vim.keymap.set("n", "<leader>ac", pi.close, { desc="pi: Close" })
vim.keymap.set({ "n", "x" }, "<leader>al", pi.paste_cursor_location, { desc="pi: Paste cursor location" })
vim.keymap.set({ "n", "x" }, "<leader>ar", pi.paste_selection_location, { desc="pi: Paste range location" })
vim.keymap.set({ "n", "x" }, "<leader>ap", pi.paste_selection_contents, { desc="pi: Paste selection contents" })

See Methods for other functionality you can map.

API Reference

Methods

Some methods take a callback as an optional cb parameter that is called on success, failure, and timeout:

pi.paste_selection_location(function(result)
---@fieldokboolean---@fieldreason ("command_success"|"command_failure"|"timeout"|"error")?---@fielderrorstring?---@fieldvalueany?ifresult.okthenvim.notify("Successfully pasted range location, focusing pi")
pi.focus()
else-- handle failure if neededendend)

pi.on_blocking will let you modify pi behaviour through the return value, though very complicated (or security critical) use-cases are often better off as an actual pi extension. The input event and the return value (in the case of pi_blocking) are JSON-serialized over the socket.

MethodDescription
get_surface(name)Get the Lua surface module
start()Start pi, or focus if already started
on(name, function(event) ... end)Listen to any pi event (asynchronously)
on_blocking(name, function(event) ... end)Same as above but sends the return value back to pi to be returned in the real event handler
focus(cb)Focus on the pi instance
paste_cursor_location(cb)Paste cursor location (file:linenum) into pi prompt
paste_selection_location(cb)Paste range location (file:startline-endline) into pi prompt
paste_selection_contents(cb)Paste the current selection contents into your pi prompt
paste_qflist(cb)Paste your quickfix list into pi prompt
close(cb)Close the window/tab pi is running in (will kill pi process unless surface is nvim)
stop()Stop the socket server

Options

require("pi-agent").setup({
-- defaults to nvim surface if nilsurface=nil, -- set a custom pi binary pathpi_bin="pi",
-- whether to focus pi when it startsfocus_on_open=true,
-- whether to close the pi window/tab on disconnectclose_on_disconnect=false,
-- tools configurationtools= {
disable_all=false,
nvim_get_qflist= {
enabled=true,
},
nvim_set_qflist= {
enabled=true,
-- called when the qflist is updatedon_update=nil,
},
},
})

You can configure surfaces by calling .configure() on them, which also returns the module itself for convenience.

Here we make pi open in a new Neovim tab instead of a vsplit, and automatically enter insert on focus:

pi.setup({
surface=pi.get_surface("nvim").configure({
open_in="tab", -- open pi in a new tab instead of a vsplitauto_insert_on_focus=true, -- automatically enter insert mode when navigating to pi-- etc...
})
})

Surface implementations are just Lua modules, so you can provide your own shaped like pi_agent.Surface.

Surface options

nvim surface options

{
-- window | tab -- where to open piopen_in="window",
-- set up autocmd that enters insert mode when focusing on piauto_insert_on_focus=true,
-- right | left | top | bottom -- which way to splitsplit="right",
-- what size to give the pi window in a splitsize_ratio=0.4,
}

herdr surface options

{
-- set a custom herdr binaryherdr_bin="herdr",
}

tmux surface options

{
-- set a custom tmux binary pathtmux_bin="tmux",
}

Recipes

Automatically focus on successful commands

vim.keymap.set({ "n", "x" }, "<leader>al", function()
pi.paste_cursor_location(function(res)
ifres.okthenpi.focus()
endend)
end, { desc="pi: Paste line location" })

Open/close quickfix list on populate/clear

pi.setup({
tools= {
nvim_set_qflist= {
on_update=function()
vim.cmd(#vim.fn.getqflist() ~=0and"copen" or"cclose")
end,
},
},
})

Sending arbitrary text to pi

Part of the internal API, subject to change.

pi.send(cbornil, "command", "append_text", { lines= { "list", "of", "lines" }, as_paragraph=false})

Planned

  • LSP tools (nvim_get_diagnostics)
  • Compose pi messages in a Neovim scratch buffer
  • Support for more surfaces like cmux and zellij
  • I did experiment with tools for marks/registers but they proved to be relatively useless?

About

Pragmatic nvim + pi agent integration

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages