Run a fleet of coding agents from Neovim — pi, Claude Code, or any agent CLI — each in its own native terminal, listed and switchable from a clean board.
Inspired by claude agents, but with one hard rule:
The agent runs as its real CLI, in a native nvim terminal. Nothing reimplements the agent's UI.
So the whole conversation lives in a buffer you can scroll, search and yank with your own keybindings.
- Launch agents into a native terminal, with an optional initial prompt.
- Persist & resume — a roster on disk; reopen a past agent (focus it if live, else relaunch it via its CLI's resume flag in its original cwd).
- List & switch the agents of the current directory, live ones merged with on-disk sessions (pi and Claude) and deduped.
- Live board (
:AgentsBoard) — a dedicated buffer grouping agents into running / idle / done / archived sections, with colored state and relative last-activity times, refreshing on a timer. - Lifecycle — mark done, archive (soft, never deletes a session), rename, stop; bulk actions over a visual selection.
- Background auto-naming (opt-in) — name an agent from its launch prompt, optionally via a one-shot LLM namer.
See ROADMAP.md for what's shipped and what's planned. Today pi and Claude Code are supported end-to-end; other CLIs launch but aren't yet persisted/resumed.
- Neovim 0.9+
- At least one agent CLI on your
PATH(pi,claude, …)
With lazy.nvim:
{
"MateoGreil/agent-fleet.nvim",
keys = {
{ "<leader>aa", "<cmd>Agent<cr>", desc = "Agent Fleet: launch agent" },
{ "<leader>al", "<cmd>Agents<cr>", desc = "Agent Fleet: list & switch" },
{ "<leader>ab", "<cmd>AgentsBoard<cr>", desc = "Agent Fleet: board" },
{ "<leader>ad", "<cmd>AgentDone<cr>", desc = "Agent Fleet: done" },
{ "<leader>ax", "<cmd>AgentArchive<cr>", desc = "Agent Fleet: archive" },
{ "<leader>as", "<cmd>AgentSend<cr>", desc = "Agent Fleet: send selection to agent" },
{ "<leader>as", ":AgentSend<cr>", mode = "x", desc = "Agent Fleet: send selection to agent" },
},
config = function()
require("agent-fleet").setup({
agents = {
pi = {},
claude = {},
},
-- optional: background auto-naming via LLM
auto_name = {
enabled = true,
model = "anthropic/claude-haiku-4-5",
},
})
end,
}With packer.nvim:
use({
"MateoGreil/agent-fleet.nvim",
config = function()
require("agent-fleet").setup({
agents = {
pi = {},
claude = {},
},
-- optional: background auto-naming via LLM
auto_name = {
enabled = true,
model = "anthropic/claude-haiku-4-5",
},
})
local map = vim.keymap.set
map("n", "<leader>aa", "<cmd>Agent<cr>", { desc = "Agent Fleet: launch agent" })
map("n", "<leader>al", "<cmd>Agents<cr>", { desc = "Agent Fleet: list & switch" })
map("n", "<leader>ab", "<cmd>AgentsBoard<cr>", { desc = "Agent Fleet: board" })
map("n", "<leader>ad", "<cmd>AgentDone<cr>", { desc = "Agent Fleet: done" })
map("n", "<leader>ax", "<cmd>AgentArchive<cr>", { desc = "Agent Fleet: archive" })
map("n", "<leader>as", "<cmd>AgentSend<cr>", { desc = "Agent Fleet: send selection to agent" })
map("x", "<leader>as", ":AgentSend<cr>", { desc = "Agent Fleet: send selection to agent" })
end,
})setup() is required, and you must declare at least one agent — there are
no default agents. pi and claude are recognized keys that auto-fill their
command and session settings, so pi = {} / claude = {} is enough; pass extra
fields to override. See Configuration.
:Agent " launch the default agent, prompting for an initial message (like the board's i)
:Agent fix auth " launch the default agent with "fix auth" as the initial prompt
:AgentResume " reopen a past agent of this directory (focus if live, else resume)
:Agents " list & switch agents of this directory (focus if live, else resume)
:AgentsBoard " open the live board buffer (sections, colors, per-row keymaps)
:AgentDone " mark an agent done (✓)
:AgentArchive " archive / unarchive an agent (hidden from :Agents by default)
:AgentRename foo " rename the current agent (or pick one) to "foo"
:AgentRename " rename via a prompt (current agent, or pick one)
:AgentSend " send the current line / visual selection to an agent as a file:line ref:AgentSend doesn't send file content — it sends a path:line (or
path:line1-line2 in visual mode) reference into the current or
last-focused agent's terminal input, without moving focus or pressing Enter
for you. That means you can fire it from a few different spots to stack up
several references in the agent's input, then switch over yourself, add your
question, and press Enter.
The visual-mode mapping must be :AgentSend<cr> (not <cmd>AgentSend<cr>): a
<Cmd> mapping does not pass the visual selection's line range to the command,
so it would only send the cursor line. The leading : lets Neovim insert the
'<,'> range for the selection.
Inside an agent terminal: <C-\><C-n> to enter Normal mode, then move / scroll /
yank with your usual nvim keys. i / a to type to the agent again.
A dedicated, non-terminal buffer that opens in the current window and lists
this directory's agents in lifecycle sections — running, idle, done,
and (when toggled) archived — each row showing a live/dead marker, the
derived state, the name, and a relative last-activity time. It re-renders on a
timer while visible and reacts to agents exiting. Move with your normal nvim
keys (j/k///gg); the per-row actions are:
| Key | Action |
|---|---|
<CR> |
switch to the agent under the cursor (focus its terminal if live, else resume it via its CLI) |
d |
mark done (✓) |
x |
archive / unarchive |
r |
rename (prompt) |
s |
stop — kill the live terminal without marking it done (still resumable) |
a |
launch a new agent |
i |
type a prompt, then launch a new agent started with it |
A |
toggle the archived section |
R / gr |
refresh now |
Switching, a and i hand the board's window to the agent (the board buffer
is wiped; reopen with :AgentsBoard). Action keys are no-ops on section
headers. d, x and s also work over a visual line selection (V): select a
span of rows and the action applies to every agent in it at once. There is
intentionally no q binding — leave the board with your usual buffer navigation.
require("agent-fleet").setup({
default_agent = "pi", -- which agent `:Agent` launches with no argument
agents = { -- registry of agents: key -> { cmd = "<command>" }
pi = {},
claude = {},
},
window = "enew", -- where the agent terminal opens (see below)
start_insert = true, -- drop straight into terminal insert mode
follow_output = true, -- keep unfocused agent terminals scrolled to the bottom
board = { -- the :AgentsBoard buffer
refresh_ms = 2000, -- how often the open board re-renders
},
auto_name = { -- background auto-naming (off by default; see below)
enabled = false,
model = nil, -- e.g. "openai/gpt-4o-mini"; required to do anything
},
})| Option | Default | Description |
|---|---|---|
default_agent |
(required when multiple agents declared) | Agent launched by :Agent with no argument. Implicit when exactly one agent is declared; required when two or more are declared; with zero agents declared the plugin notifies an error and commands no-op. |
agents |
(required — none by default) | Registry of declared agents (key -> { cmd = … }). You must declare at least one; pi and claude are recognized keys that auto-fill their presets ({} suffices). |
window |
"enew" |
Ex command that opens the agent window. |
start_insert |
true |
Enter terminal insert mode after launching. |
follow_output |
true |
Auto-scroll an agent's terminal to the bottom on new output even when its window is not focused. |
board.refresh_ms |
2000 |
How often (ms) the open :AgentsBoard re-renders. |
The agents registry maps a key to a command. pi and Claude Code are
fully supported end-to-end (persist, resume, board listing with live state).
Other CLIs will launch in a terminal but not appear on the board or be
resumable unless they provide a session backend.
agents = {
pi = { cmd = "pi" },
claude = { cmd = "claude" },
}default_agent chooses which of these :Agent launches; the agent type is not
a command argument (all of :Agent's arguments become the new agent's name).
cmd is split on spaces into an argv list and executed directly without a
shell, so each token becomes a separate argument — no quoting, pipes, or
VAR=val env prefixes. If you need shell features, point cmd at a wrapper
script.
Declaring pi = {} or claude = {} pulls in that agent's built-in preset
(command, session flags, and on-disk session location); any field you set
overrides the preset. A key with no matching preset (e.g. aider = { cmd = "aider" }) launches and is tracked while live, but is only persisted/resumed if
you also give it a session = { id_flag, name_flag, resume_flag } block.
window is run as a plain Ex command right before the buffer becomes a
terminal, so any window-opening command works. Common choices:
| Value | Result |
|---|---|
"enew" |
current window (default) |
"botright vnew" |
new vertical split on the right |
"topleft vnew" |
new vertical split on the left |
"topleft new" |
new horizontal split on top |
"botright new" |
new horizontal split on the bottom |
"tabnew" |
new tab |
Power users can pass any Ex command, e.g. window = "botright 80vnew" for a
fixed-width split.
When an agent (pi or Claude) is launched with an initial prompt but without
a name (board i, or :Agent <prompt>), the plugin can rename it from that
prompt: it runs a lightweight one-shot namer for that agent's backend (pi with
tools/session/extensions off, or claude -p with --tools "") on the prompt
text, sanitizes the reply to a short name, and applies it. There is no
polling — the prompt we launched with is used directly. The agent stays
machine-named, so a manual :AgentRename always takes precedence and is never
overwritten.
Independently of this LLM namer, an agent launched with a prompt but no name
already gets a sensible default: the first line of the prompt (char-aware
truncated to 40 chars), instead of the numbered <kind>-<n> name. An agent
launched without a prompt still falls back to <kind>-<n>.
The LLM namer is OFF by default and does nothing unless you both set
auto_name.enabled = true and provide a model.
auto_name = {
enabled = false, -- master switch
model = nil, -- model the namer runs with (required, e.g. "openai/gpt-4o-mini")
thinking = "off", -- pi --thinking value for the namer
namer_timeout_ms = 30000, -- kill the namer subprocess after this long
max_chars = 2000, -- cap the prompt text sent to the namer
}| Option | Default | Description |
|---|---|---|
enabled |
false |
Master switch for background auto-naming. |
model |
nil |
Model the one-shot namer runs with. Required. |
thinking |
"off" |
pi --thinking value for the namer. |
namer_timeout_ms |
30000 |
Kill the namer subprocess after this long. |
max_chars |
2000 |
Cap on the prompt text sent to the namer. |
The namer is the only subprocess agent-fleet spawns itself; it runs via
jobstart with an argv list (no shell), and it never passes --name or touches
the session file.
The board defines these highlight groups, each linked to a standard group by
default so it follows your colourscheme; override any with
vim.api.nvim_set_hl(0, "<group>", { … }):
| Group | Default link | Used for |
|---|---|---|
AgentFleetWorking |
DiagnosticInfo |
a working agent's state |
AgentFleetIdle |
Normal |
an idle agent's state |
AgentFleetStopped |
Comment |
a stopped agent's state |
AgentFleetError |
DiagnosticError |
an errored agent's state |
AgentFleetNew |
DiagnosticHint |
a brand-new agent's state |
AgentFleetUnknown |
NonText |
state older than the tail read |
AgentFleetArchived |
Comment |
archived rows (dimmed) |
AgentFleetHeader |
Title |
section headers and the empty-state title |
AgentFleetTime |
Comment |
the relative last-activity column |
