A small Neovim chat interface for Pi. It supports streaming responses, mid-turn steering, tool status, multi-turn conversations, and visual-selection context.
- Neovim 0.11+
- snacks.nvim
- Pi installed and configured with an AI provider
npm install -g @mariozechner/pi-coding-agentUsing lazy.nvim
{
"AnonymousMorris/ai.nvim",
dependencies = { "folke/snacks.nvim" },
event = "VeryLazy",
opts = {},
}VeryLazy loads the plugin automatically after Neovim finishes its initial startup work.
Using packer.nvim
use {
"AnonymousMorris/ai.nvim",
requires = { "folke/snacks.nvim" },
config = function()
require("ai").setup()
end,
}Using mini.deps
MiniDeps.add({
source = "AnonymousMorris/ai.nvim",
depends = { "folke/snacks.nvim" },
})
require("ai").setup()With another package manager, install both ai.nvim and snacks.nvim, then call require("ai").setup() after they are available on Neovim's runtime path.
No global keymaps by default. You choose.
-- Open or return to the current chat
vim.keymap.set("n", "<leader>ai", "<Cmd>AI<CR>", { desc = "Open AI chat" })
-- Open chat with the visual selection as context
vim.keymap.set("x", "<leader>ai", "<Cmd>AISelection<CR>", { desc = "Open AI chat with selection" })| Command | Action |
|---|---|
:AI |
Open or return to the current chat |
:AISelection |
Add the visual selection to the chat input |
:AIStop |
Stop the agent and delete the current session |
Switching to the input enters insert mode automatically. The transcript and input use separate rounded windows, with a configurable contextual hint bar below them. In the input, <Enter> sends the prompt and focuses the transcript. If the AI is still processing the current turn, <Enter> sends the message as a steering instruction instead. <S-Enter> inserts a newline, and <C-c> clears a non-empty prompt; press <C-c> with an empty prompt to interrupt the current AI turn. Press <Esc> while focused on the transcript to close the chat. Closing the chat window keeps the session alive. Run :AI to reopen it or :AIStop to end it.
Selection context is inserted with its file and line range. Context blocks are folded by default.
The expanded lazy.nvim configuration below shows the plugin's actual defaults. It is equivalent to the shorter opts = {} setup above:
{
"AnonymousMorris/ai.nvim",
dependencies = { "folke/snacks.nvim" },
event = "VeryLazy",
opts = {
backend = "pi",
binary = "pi",
extensions = true,
skills = false,
skill_paths = {},
thinking = "off",
reload = true,
auto_close = true,
show_status = true,
close_delay = 1000,
chat = {
show_hints = true,
hints = {
input = {
{ key = "⏎", label = "send/steer" },
{ key = "S-⏎", label = "newline" },
{ key = "C-c", label = "clear/interrupt" },
{ key = "C-n", label = "new" },
{ key = "Tab", label = "switch" },
},
display = {
{ key = "Tab", label = "input" },
{ key = "Esc", label = "close" },
},
},
keys = {
input = {
["<M-CR>"] = {
"insert_newline",
mode = "i",
desc = "Insert newline",
},
["<S-CR>"] = {
"insert_newline",
mode = "i",
desc = "Insert newline",
},
["<C-c>"] = {
"clear_or_interrupt",
mode = { "i", "n" },
desc = "Clear input or interrupt current AI turn",
},
["<C-n>"] = {
"new_session",
mode = { "i", "n" },
desc = "Create a new AI session",
},
["<C-w>k"] = {
"focus_display",
desc = "Focus AI chat display",
},
["<C-w><C-k>"] = {
"focus_display",
desc = "Focus AI chat display",
},
["<Tab>"] = {
"focus_display",
mode = { "i", "n" },
desc = "Focus AI chat display",
},
},
display = {
["<C-w>j"] = {
"focus_input",
desc = "Focus AI chat input",
},
["<C-w><C-j>"] = {
"focus_input",
desc = "Focus AI chat input",
},
["<Tab>"] = {
"focus_input",
desc = "Focus AI chat input",
},
["<Esc>"] = {
"close",
desc = "Close AI chat",
},
},
},
},
},
}Without lazy.nvim, pass the contents of opts above to require("ai").setup().
Pi runs in RPC mode with session persistence disabled. Extensions are enabled by default.
After each completed AI turn, reload = true reloads every loaded file buffer whose file changed on disk during that turn. This keeps Neovim synchronized with agent edits, including replacing unsaved buffer contents when the agent changed the same file. Set reload = false to disable this behavior.
Set chat.show_hints = false to hide the contextual hint bar. The chat.hints.input and chat.hints.display lists are rendered exactly in their configured order. Set chat.keys = false to disable all chat-specific keymaps, or set chat.keys.input or chat.keys.display to false to disable one group.
No skills are bundled or enabled by default.
skills = trueenables Pi's skill discovery, including~/.agents/skills/.skill_paths = { ... }loads localSKILL.mdfiles or directories, even withskills = false.
Only a leading ~/ is expanded. Other path text is passed to Pi unchanged, and relative paths use the agent's working directory.
Pi loads skill instructions when needed. To invoke a skill explicitly, send /skill:name followed by your request.
Append text, file contents, or both:
require("ai").setup({
append_system_prompt = "Keep responses concise.",
append_system_prompt_filepath = "~/.config/nvim/ai/writing.md",
})Both options are optional and accept a string or list of strings. Text comes first, then file contents, in list order with blank lines between entries.
To append multiple prompts:
require("ai").setup({
append_system_prompt = {
"Keep responses concise.",
"Explain your changes.",
},
append_system_prompt_filepath = {
"~/.config/nvim/ai/writing.md",
"~/.config/nvim/ai/project.md",
},
})Files are read when opening a new session, including with Ctrl-N. Missing or unreadable files stop startup with an error notification. Only a leading ~/ is expanded; relative paths use the agent's working directory.
The plugin passes a nonempty combined prompt through an owner-only temporary file, so its size does not count against the operating system's command-line limit. append_system_prompt text stays literal even when it matches an existing filename.
The separate system_prompt option replaces Pi's default prompt. When its value names an existing path, Pi keeps its file-input behavior and ai.nvim leaves that file in place. Other nonempty values, including large literal prompts, use an owner-only temporary file. The plugin removes its temporary files when the backend exits, the session is stopped, or startup fails. Model context limits still apply.
Unslop by Lauren Tan removes common AI writing patterns. It is MIT licensed.
With Unslop installed in ~/.agents/skills/unslop/:
require("ai").setup({
skill_paths = {
"~/.agents/skills/unslop/SKILL.md",
},
append_system_prompt_filepath = "~/.config/nvim/ai/unslop.md",
})Unslop disables automatic model invocation. Use /skill:unslop, or create ~/.config/nvim/ai/unslop.md with:
Before writing or editing prose, read `~/.agents/skills/unslop/SKILL.md` and follow its Unslop instructions.for test in tests/*_e2e.lua; do
nvim --headless -u NONE -l "$test" || exit 1
doneSet SNACKS_NVIM to the snacks.nvim checkout path if it is not installed at Neovim's default lazy.nvim data path.
