Skip to content

Repository files navigation

herdr-context.nvim

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.

Requirements

  • Neovim 0.10 or newer
  • Herdr 0.7.5 or newer
  • jq for 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.

Installation

Install the Herdr side:

herdr plugin install makyinmars/herdr-context.nvim

Herdr 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 = {},
}

Commands

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.

Configuration

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.

Context composer

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.

Delegating to a new agent

: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 when submit = true);
  • S: explicitly send and submit now, regardless of the submit default;
  • p: toggle the full payload preview;
  • h: inspect the session staging history;
  • <Tab>: switch between checklist and preview panes;
  • ?: show the key reference;
  • q or <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.

Live presence

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 · 3

For 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> or t: 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.

Target selection

The shared snapshot supplies live agent and layout metadata. Candidates are ranked by:

  1. same tab;
  2. same workspace;
  3. same exact worktree;
  4. another worktree from the same repository;
  5. same working directory;
  6. same Git root;
  7. 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.

Payloads

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 contract

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-text as 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 prompt as one argv element;
  • payload size is checked before target resolution or transport.

Keep submit = false unless automatic submission is explicitly desired.

Development

make test
make lint
# Read-only integration check against a running Herdr server:
make test-live

The 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.

About

From Neovim, select code or stand on a line, choose a live Herdr agent, and stage structured context in that agent’s prompt—without submitting it.

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages