See live Herdr agents inside Neovim and stage code context in their prompts without submitting it.
herdr-context.nvim is one repository with two install surfaces:
- a Neovim plugin for collecting, formatting, and staging context;
- a Herdr companion plugin with a transient popup for pinning the default target agent.
- Neovim 0.10 or newer
- Herdr 0.7.5 or newer
jqfor the optional Herdr popup target picker on Linux and macOS- Windows PowerShell 5.1 or newer for the optional picker on Windows
Neovim should normally be running in a Herdr pane so HERDR_PANE_ID, HERDR_TAB_ID, and
HERDR_WORKSPACE_ID are available.
Install the Herdr side:
herdr plugin install makyinmars/herdr-context.nvimHerdr 0.7.5 stores installed and linked plugins globally. If this companion plugin was installed only inside a named Herdr 0.7.3 session, run the install command again after upgrading.
Install the Neovim side with lazy.nvim:
{
"makyinmars/herdr-context.nvim",
cond = vim.env.HERDR_ENV == "1",
lazy = false, -- keeps :checkhealth herdr-context discoverable before the first mapping
opts = {},
keys = {
{
"<leader>ac",
function()
require("herdr-context").compose()
end,
mode = { "n", "v" },
desc = "Compose Herdr Context",
},
{
"<leader>ap",
function()
require("herdr-context").prompt()
end,
mode = { "n", "v" },
desc = "Prompt Herdr with Code Context",
},
{
"<leader>ay",
function()
require("herdr-context").reference()
end,
mode = { "n", "v" },
desc = "Send Reference to Herdr Agent",
},
{
"<leader>aY",
function()
require("herdr-context").send()
end,
mode = { "n", "v" },
desc = "Send Context to Herdr Agent",
},
{
"<leader>ad",
function()
require("herdr-context").diagnostics()
end,
mode = { "n", "v" },
desc = "Send Diagnostics to Herdr Agent",
},
{
"<leader>at",
function()
require("herdr-context").select_target()
end,
desc = "Select Herdr Agent",
},
{
"<leader>aa",
function()
require("herdr-context").agents()
end,
desc = "Toggle Herdr Agents",
},
{
"<leader>ar",
function()
require("herdr-context").refresh()
end,
desc = "Refresh Herdr Agents",
},
},
}For local development, point both systems at the same checkout:
herdr plugin link /path/to/herdr-context.nvim{
dir = "/path/to/herdr-context.nvim",
cond = vim.env.HERDR_ENV == "1",
opts = {},
}| Command | Behavior |
|---|---|
:HerdrContextReference |
Stage @path#L10-L20 |
:HerdrContextSend |
Stage the reference and selected code |
:HerdrContextDiagnostics |
Stage diagnostics for the current line or selection |
:HerdrContextCompose [preset] |
Collect, preview, and stage a combined context bundle |
:HerdrContextPrompt |
Open directly in the message editor with the current line or Visual selection attached |
:HerdrContextDelegate <kind> [preset] |
Create an agent and delegate a reviewed composer bundle |
:HerdrContextSymbol |
Stage the innermost symbol under the cursor |
:HerdrContextHunk |
Stage the Git hunk under the cursor |
:HerdrContextQuickfix |
Stage the current quickfix list |
:HerdrContextLocationList |
Stage the current window's location list |
:HerdrContextTarget |
Choose or change the destination agent |
:HerdrContextAgents |
Toggle the live agent drawer |
:HerdrContextExplainAgent |
Explain how Herdr detected an agent and assigned its state |
:HerdrContextHistory |
Inspect, clear, or restage session history |
:HerdrContextRefresh |
Force a cached-state refresh |
:checkhealth herdr-context |
Check Neovim, environment, Herdr, agents, and the companion plugin |
The range-aware context commands accept an Ex range. Lua calls made from Visual mode preserve linewise, characterwise, reversed, and blockwise selections.
require("herdr-context").setup({
submit = false,
focus_after_send = false,
max_payload_bytes = 64 * 1024,
target_scope = "workspace", -- "tab", "workspace", "project", or "session"
remember_target = "session", -- "none", "session", or "workspace"
min_herdr_version = "0.7.5",
composer = {
layout = "float",
width = 0.92,
height = 0.8,
checklist_width = 0.38,
provider_timeout_ms = 1500,
hunk_context_lines = 3,
preview = true,
defaults = {
selection = true,
symbol = true,
hunk = true,
diagnostics = true,
quickfix = false,
location_list = false,
trouble = false,
},
presets = {
debug = { "selection", "symbol", "hunk", "diagnostics" },
review = { "hunk", "diagnostics", "quickfix", "trouble" },
explain = { "selection", "symbol", "diagnostics" },
},
},
safety = {
enabled = true,
confirm_warnings = true,
exclude_patterns = { ".env", ".env.*", "*.pem", "*.key", "credentials*", "secrets*" },
secret_patterns = { -- Lua patterns
"AKIA[%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d][%u%d]",
"-----BEGIN .-PRIVATE KEY-----",
"api[_-]key%s*[:=]%s*%S+",
"token%s*[:=]%s*%S+",
"secret%s*[:=]%s*%S+",
"password%s*[:=]%s*%S+",
"gh[pousr]_%w+",
"github_pat_[%w_]+",
"xox[baprs]%-[%w%-]+",
'"type"%s*:%s*"service_account"',
"eyJ[%w_%-]+%.eyJ[%w_%-]+%.[%w_%-]+",
},
entropy_enabled = true,
entropy_threshold = 4.5,
entropy_min_length = 20,
entropy_keywords = { "key", "secret", "token", "password", "credential" },
},
history = {
enabled = true,
max_entries = 20,
},
providers = {
symbol = {
enabled = true,
lsp = true,
treesitter_fallback = true,
},
hunk = {
enabled = true,
backends = { "mini_diff", "git" },
},
trouble = {
enabled = true,
modes = { "diagnostics", "quickfix" },
},
},
presence = {
enabled = true,
socket = true,
poll_interval_ms = 3000,
reconnect_max_ms = 10000,
debounce_ms = 100,
notifications = {
idle = false,
done = false,
blocked = false,
},
},
agents_view = {
position = "right", -- "left" or "right"
width = 44,
preview_lines = 80,
deep_preview_lines = 300,
group_by = "workspace", -- "none", "workspace", or "tab"
side_preview = true,
preview_width = 64,
show_cwd = true,
show_workspace = true,
show_tab = true,
},
statusline = {
show_target = true,
show_agent_count = true,
show_connection = true,
compact = false,
icons = {
herdr = "Herdr",
target = "▶",
idle = "●",
working = "◉",
blocked = "!",
done = "✓",
unknown = "○",
disconnected = "×",
separator = "·",
},
},
})Set presence.enabled = false to disable the bootstrap snapshot, socket subscription, reconnect timers,
and polling fallback. Existing v0.1 configurations remain valid.
Additional transport options are available for unusual agents:
require("herdr-context").setup({
multiline_strategy = "auto", -- "auto", "bracketed_paste", or "context_file"
bracketed_paste_agents = {
codex = true,
claude = true,
},
context_file_dir = nil, -- defaults to stdpath("cache") .. "/herdr-context"
herdr_bin = nil, -- defaults to HERDR_BIN_PATH, then "herdr"
})In auto mode, multiline payloads for Codex and Claude use terminal bracketed-paste sequences.
Unknown agents receive a single-line reference to a temporary Markdown context file. This avoids
injecting literal newline bytes into an agent that may interpret them as Enter.
Stage-only transport uses Herdr's pane send-text API so staging remains non-submitting. In auto mode,
the bracketed-paste contract keeps both lines in the input editor while the agent remains idle. Unknown
or newly introduced agent families remain on the conservative context-file path until configured.
Explicit submission (S, <C-Enter>, or submit = true) sends the original payload through
herdr agent prompt, which validates the live agent and handles its input mode and Enter atomically.
The two-pane composer freezes the source buffer, cursor, selection, changedtick, path, and working directory before providers begin. Providers collect independently, and one timeout or failure does not block the others. The polished left panel shows the attached context, source, target, message, warnings, and byte budget; the right pane contains the exact Markdown payload that will be staged.
For the fastest code-to-agent flow, select code in Visual mode and run :HerdrContextPrompt (or map
require("herdr-context").prompt()). The message editor opens immediately inside Neovim with that exact
selection attached. Write the thought you would otherwise type in the agent, then press <C-Enter> to
send and submit it. <C-s> keeps the message and returns to the composer so you can inspect or adjust the
attached context first. In Normal mode, the same action starts from the current line and discovers the
containing symbol, hunk, and diagnostics.
Tracking is opt-in for Lua callers. It submits through agent prompt --wait and observes the agent
until it reaches idle, unseen done, or blocked:
require("herdr-context").prompt({
wait = true,
timeout_ms = 120000,
preview_result = true, -- also open output for idle/done; blocked always opens it
})Ordinary <C-Enter> remains a short-lived send and does not wait. A tracked blocked result focuses
the agent and opens its output; idle and done notify completion. A tracking timeout or
agent_prompt_stalled warning does not cancel the remote task, which may continue running.
:HerdrContextDelegate codex review opens the composer with the review preset and a new Codex agent
as its destination. After reviewing the exact bundle and pressing s or S, choose whether to split
the current tab, create a tab, or create a workspace, then choose whether to send without waiting or
wait and preview the result. Herdr creates an unfocused shell pane, starts a uniquely named reviewer
with agent start, selects it as the context target, and submits the bundle with agent prompt.
Lua callers can bypass either picker and customize startup:
require("herdr-context").delegate({
kind = "codex",
preset = "review",
name = "reviewer", -- receives a numeric suffix when already live
placement = "tab", -- "split", "tab", or "workspace"; omit to ask
direction = "right", -- split placement only
wait = true, -- omit to ask
timeout_ms = 120000,
startup_timeout_ms = 30000,
preview_result = true,
agent_args = { "--model", "fast" }, -- passed after `agent start ... --`
})Cancelling either choice leaves the composer open. Accepting both choices freezes the reviewed bundle
and closes the composer before any side effect; optional lifecycle tracking then continues in the
background. Post-creation failures identify the retained pane or agent instead of deleting it or
blindly retrying. An agent_name_taken race retries a unique name in the same pane, while startup and
prompt timeouts are reported without cancelling or duplicating the remote process.
Normal mode selects the innermost symbol, the hunk under the cursor, and diagnostics scoped to the symbol (then the hunk). If neither symbol nor hunk is available, it selects the current line. Visual mode selects the exact Visual range and overlapping diagnostics, leaving symbol and hunk unchecked. Quickfix, location-list, and Trouble sources are deliberately opt-in defaults.
Composer controls are:
<Space>: toggle the provider under the cursor;i: write or edit the freehand message included at the top of the bundle;P: apply a named provider preset;t: choose a target and return to the composer;r: recapture the source and rerun providers;s: stage the exact preview (or stage and submit whensubmit = true);S: explicitly send and submit now, regardless of thesubmitdefault;p: toggle the full payload preview;h: inspect the session staging history;<Tab>: switch between checklist and preview panes;?: show the key reference;qor<Esc>: cancel.
Presets can also be selected directly with commands such as :HerdrContextCompose debug. Only available
providers are selected. i opens a multiline Markdown message editor; keep it with <C-s>, send it with
<C-Enter> (or <M-Enter> in terminals that do not distinguish Control-Enter), or cancel with q from
Normal mode. Messages are rendered as a deterministic ## Instructions section and are included in the
same byte budget and exact-preview path as provider content.
Editing the source buffer marks the preview stale and disables staging until it is refreshed. The
combined final payload—including headings and Markdown fences—is rejected when it exceeds
max_payload_bytes; sections are never silently truncated or dropped.
The symbol provider asks every eligible LSP client for document symbols, deterministically chooses the
smallest containing range, and falls back to Treesitter. The hunk provider prefers MiniDiff because it
can include unsaved changes, then uses git diff for saved buffers. Trouble is only consulted when the
plugin is loaded and a configured view is open.
Custom providers use the same timeout, preview, and byte-budget path:
require("herdr-context").register_provider({
id = "custom-build",
name = "Build output",
priority = 70,
collect = function(request, callback)
callback({
id = "custom-build",
title = "Build output",
content = "...",
format = "text",
fingerprint = "custom-build:latest",
})
end,
})collect may return a cancellation function. It must call its callback at most once with either a
normalized section or an error. Optional integrations should report unavailable state instead of
throwing; :checkhealth herdr-context summarizes the currently usable backends.
One shared state store serves the statusline, agent drawer, and target UI. Setup fetches an initial
snapshot, then subscribes to Herdr events over HERDR_SOCKET_PATH. Unix socket paths are used directly;
on Windows, bare pipe names are mapped to \\.\pipe\<name> and health checks test the named pipe by
connecting instead of treating it as a filesystem entry. If the connection drops, cached data is marked
stale, polling starts, and reconnects use exponential backoff. Polling stops after reconnect. Every pipe
and timer closes on VimLeavePre.
The statusline reads only cached Lua state; it never starts a process or performs socket I/O during a redraw:
require("herdr-context").statusline()
-- Herdr ▶ ● codex · 3For lualine:
{
"nvim-lualine/lualine.nvim",
opts = function(_, opts)
table.insert(opts.sections.lualine_x, function()
return require("herdr-context").statusline()
end)
end,
}The native agent drawer is a scratch-buffer split. Its controls are:
<CR>ort: select the pane as the context target;f: focus the Herdr pane;p: preview 80 lines of the agent's recent output;P: request the deeper 300-line transcript;e: show Herdr's agent-detection, matched-rule, lifecycle-authority, and evidence explanation;/: filter agents by name, status, workspace, tab, path, or message;<Space>: collapse or expand the workspace/tab group under the cursor;c: clear the active filter;r: force a state refresh;q: close the drawer.
Agents are grouped by workspace and tab by default. Output is read only when p or P is pressed;
the drawer never reads agent output in the background. With a Herdr socket, previews use agent.read
so truncation metadata is retained. A busy agent automatically falls back from alternate-screen
history to its live viewport, and the preview labels both that fallback and any omitted older output.
Without a socket, the text-only CLI remains the compatibility fallback. The adjacent preview uses
agents_view.preview_width; preview_lines and deep_preview_lines bound the two transcript depths.
Press r inside the output pane to refresh it.
:HerdrContextExplainAgent resolves a target and runs herdr agent explain <pane> --json. The same
view is available with e in the drawer. It reports the final state, active and cached manifest
versions, winning and evaluated rules, visible evidence, lifecycle authority, and fallback or skipped
reasons. This is Herdr's authoritative detector output; the plugin does not duplicate screen parsing.
The presence.notifications flags opt into desktop-visible Neovim notifications when an existing
agent transitions to idle, unseen done, or blocked. Initial snapshots do not notify, and all
transitions are disabled by default.
Advanced consumers can read or subscribe to immutable snapshots:
local state = require("herdr-context.state")
state.get()
state.agents({ scope = "workspace" })
local subscription = state.subscribe(function(snapshot) end)
state.unsubscribe(subscription)
state.refresh({ force = true }, function(snapshot, err) end)State changes emit User events named HerdrContextUpdated, HerdrContextTargetChanged,
HerdrContextAgentStatusChanged, HerdrContextConnected, and HerdrContextDisconnected. Relevant
event details are available through vim.v.event and autocmd callback data.
Socket presence reads the server version, opens the lifecycle subscription, and takes an authoritative
snapshot while buffering new events. It then applies pane, tab, workspace, and agent-status events
directly to the shared cache. New and removed agents gain or lose a dedicated status stream without
reconnecting the lifecycle subscription. Further full snapshots are reserved for reconnects, explicit
refreshes, unknown or inconsistent events, and backwards pane revisions. Herdr 0.8-only events such as
workspace.reordered are subscribed only when the snapshot reports a compatible version.
The shared snapshot supplies live agent and layout metadata. Candidates are ranked by:
- same tab;
- same workspace;
- same exact worktree;
- another worktree from the same repository;
- same working directory;
- same Git root;
- other agents in the session.
Herdr workspace provenance is preferred for the worktree comparisons. Working-directory and Git-root
matching remain available as lower-priority signals. target_scope filters that list before ranking:
"project" includes the current repository's worktrees and cwd/Git-root matches, while "tab",
"workspace", and "session" retain their narrower or broader meanings. The current Herdr pane is
excluded. Pane IDs are used internally because labels such as codex are not necessarily unique. When
Herdr changes a workspace-qualified pane ID during a move, the session selection and any persisted
workspace pins are migrated to the new ID.
The selected pane is still checked against a fresh snapshot before every send. With the default
remember_target = "session", the picker reopens whenever multiple agents are live instead of silently
reusing the previous destination. A sole remaining candidate is selected when auto_select = true.
vim.ui.select drives the picker, so existing Snacks integrations are honored.
The Herdr companion action herdr-context.pin-target on Linux/macOS, or
herdr-context.pin-target-windows on Windows, opens an 80%-wide, 20-row popup picker. The popup is
transient: it does not join the tiled layout, appear in agent snapshots, or emit pane lifecycle events,
and it closes when the picker exits. The Bash picker uses jq; the Windows picker uses only the bundled
Windows PowerShell runtime. It stores one pane ID per workspace in the plugin config directory. Neovim
reads the same file. Set remember_target = "workspace" to make Neovim selections update it too and
keep that explicit pin across sends, or set HERDR_CONTEXT_CONFIG to override the shared file path.
Reference only:
@lua/plugins/snacks.lua#L53-L60
Reference with content:
@lua/plugins/snacks.lua#L53-L60
```lua
zen = {
toggles = {
dim = true,
},
}
```
Paths are relative to the Git root, falling back to Neovim's working directory. Modified buffers are
marked (unsaved changes). Content from unnamed buffers is allowed, but reference-only mode rejects it
because it has no stable path. Markdown fences expand past the longest backtick run in the selection.
Drive-letter and UNC paths are normalized and compared case-insensitively on Windows; context-file
references use forward slashes so they remain unambiguous in agent prompts. Payloads over
max_payload_bytes are rejected rather than truncated.
Diagnostics include severity, source, code, message, and source line:
Diagnostics for @src/index.ts#L18-L27:
- ERROR [typescript:2345] L21: Argument is not assignable…
- WARN [eslint:no-unused-vars] L24: `result` is assigned but never used.
Safety exclusions are applied before bundle construction. Current-buffer sections matching
safety.exclude_patterns are blocked, while matching items in list providers are removed and reported.
Selected content is also checked against safety.secret_patterns. The composer shows warnings and
requires a second s press after review; direct staging commands use an explicit confirmation picker.
Changing the payload invalidates an earlier confirmation. Safety checks never print the matched secret.
The default patterns cover AWS keys, private keys, common assignments, GitHub and Slack tokens, GCP
service-account JSON, and JWTs. Entropy scanning also warns about long, unformatted values when a
secret-related word or identifier part is on the same line.
Successful stages are retained in memory up to history.max_entries. :HerdrContextHistory can inspect
the exact payload, clear the list, or restage an entry. History is never written to disk and disappears
when Neovim exits.
The transport safety guarantees remain:
Default sends never submit:
- context is passed to
herdr pane send-textas one argv element; - no shell-concatenated command is used;
- multiline input is bracketed-pasted only for configured agents, otherwise staged through a context file;
- explicit submission passes the original payload to agent-aware
herdr agent promptas one argv element; - payload size is checked before target resolution or transport.
Keep submit = false unless automatic submission is explicitly desired.
make test
make lint
# Read-only integration check against a running Herdr server:
make test-liveThe test suite also covers deterministic bundles, provider timeout and cancellation, LSP symbol fixtures, MiniDiff add/change/delete hunks, Git diff parsing, quickfix normalization, stale composer buffers, exact preview rendering, and combined byte budgets. Transport tests use a fake Herdr executable; presence tests use sanitized socket fixtures and fake clients. Shell smoke tests exercise the companion popup launcher and manifest sizing, target ranking, and workspace target persistence. Windows CI separately covers drive and UNC paths, named-pipe endpoints and presence probes, exact multiline/modified-key transport, and the PowerShell companion picker.