The vocabulary code-preview.nvim is written in. When naming things in commits, issues, tests, docs, or code, prefer the terms defined here over synonyms.
An external AI coding CLI that proposes file edits and asks the user for permission before applying them. The plugin's reason for existing: previewing those proposals inside Neovim before the agent acts.
Supported agents today: Claude Code, OpenCode, Codex CLI, GitHub Copilot CLI. Each one fires hooks (or the agent's equivalent) on every proposed edit, which the plugin intercepts.
The directory backends/ and the env var CODE_PREVIEW_BACKEND are historical names for what we now call an agent. Don't rename them — the cost outweighs the benefit — but in conversation, design docs, and new code, say "agent."
The per-agent adapter that translates that agent's hook format into the plugin's normalised core. One integration per agent. Each integration has two parts:
- Installer (
lua/code-preview/backends/<agent>.lua) — wires the agent's config files (e.g..claude/settings.local.json,.opencode/plugins/index.ts) to point at the plugin's hook scripts. - Hook entry (
bin/hook-entry.{sh,ps1}, one generic shim per OS) — see Hook entry.
When a doc or issue says "the Codex integration," it means the installer + adapter scripts for Codex — never the running Codex CLI itself.
One agent action that wants to touch the filesystem — a single firing of the agent's pre-tool hook. Corresponds 1:1 with an Edit / Write / MultiEdit / Bash / ApplyPatch invocation by the agent.
A proposal can produce zero, one, or many previews:
- Edit / Write / MultiEdit — one preview per call (single file).
- ApplyPatch — one preview per file in the patch.
- Bash — zero previews; the plugin only updates change indicators (no content diff is rendered).
Do not say "tool use" or "tool call" in plugin-side docs — those are agent-side terms. The plugin sees proposals.
The per-file unit of pending review. One open diff tab (or vsplit, or inline buffer) keyed by absolute file path in active_diffs. A preview contains a diff; "diff" is the content (added/removed lines), "preview" is the thing the user opens, scrolls, and closes.
Lifecycle: a proposal creates one or more previews → the user reviews them → the agent's post-tool hook closes the matching previews (whether the user accepted or rejected on the agent's side).
Prefer "preview" over "diff" when naming the unit: close_for_file closes a preview; the buffer inside it displays a diff.
The transport that lets an integration's shell scripts call into the running Neovim. Implemented by bin/nvim-call.sh (caller side) and lua/code-preview/rpc.lua (dispatcher side): a JSON args array is written to a tempfile, then luaeval invokes the named module function with those args decoded.
A single request → response invocation is an RPC call. We do not yet need a more specific name for the call shape; #47 may still reshape it.
The act of finding which running Neovim instance to address an RPC call at. Resolution order (see bin/nvim-socket.sh):
$NVIM_LISTEN_ADDRESSenv var, if its socket is responsive.- Pidfile lookup — preferred path since #47 phase 1.
- Glob
/var/folders/*/T/nvim.*/0(macOS default tempdir). - Glob
/tmp/${NVIM_APPNAME}.*/0(Linux + some macOS setups). - Glob
$XDG_RUNTIME_DIR/${NVIM_APPNAME}.*.0(NixOS, systemd-based distros).
If multiple instances match, prefer the one whose cwd matches (or is a parent of) the project cwd passed in by the calling hook.
On Windows (issue #46) the addressable target is a named pipe (\\.\pipe\nvim.<pid>.0) rather than a Unix socket. The pidfile is the primary path; the Unix glob fallbacks (steps 3–5) are replaced by a single pipe-enumeration fallback — list \\.\pipe\, keep names matching nvim.*, probe each for responsiveness. Pipe enumeration cannot run the cwd tiebreak (Windows has no /proc or lsof to read a process's cwd, and a pipe found this way has no pidfile cwd line), so it degrades to "first responsive pipe," mirroring how the Unix glob path degrades when the cwd lookup fails. The stale-pipe responsiveness probe (nvim --server <pipe> --remote-expr "1") is unchanged in shape; only the is-socket precheck is dropped (named pipes have no reliable existence test).
One file per running Neovim that has called code-preview.setup(). Path: ${XDG_STATE_HOME:-$HOME/.local/state}/code-preview/sockets/<pid>. Contents: line 1 is the RPC socket path, line 2 is the Neovim's cwd.
Pidfiles self-register on setup(), refresh on DirChanged, and are removed on VimLeavePre. Crashed Neovims leave stale pidfiles behind; socket discovery self-heals by probing each socket with --remote-expr "1" before using it.
The pidfile directory is computed independently — and must agree byte-for-byte — on both the Lua writer (pidfile.lua) and the shim reader, so it can only use values both sides can derive without an RPC. On Windows (issue #46) that base is %LOCALAPPDATA%\code-preview\sockets (not the Unix $XDG_STATE_HOME/$HOME formula, which yields a driveless garbage path on Windows); line 1 of the file is the named-pipe path instead of a socket path.
The pidfile is one of several socket discovery paths, not a synonym for socket discovery.
The script the agent invokes directly when it's about to (or has just) used an editing tool. One generic shim per OS, shared by all agents (ADR-0008): bin/hook-entry.sh on Unix and bin/hook-entry.ps1 on Windows, invoked as hook-entry <backend> <pre|post>. (Before the consolidation each agent had its own backends/<agent>/code-{preview,close}-diff.{sh,ps1} pair.)
Job: take the agent's native hook payload, optionally fast-path-filter noisy tools (a backend-keyed branch inside the shim; the normalisers tool map is the source of truth), discover the running Neovim, splice the payload + backend name, and make one RPC into the core handler.
The hook entry is per-OS because that is a language boundary: a .sh shim on Unix, a PowerShell .ps1 shim on Windows (issue #46). PowerShell is the single Windows logic language — the only stock-Windows-11 tool that parses JSON natively, enumerates named pipes, and probes the RPC socket; Windows PowerShell 5.1 (powershell.exe) is the floor, not pwsh 7. The installer writes the interpreter explicitly into the agent's command field (powershell -NoProfile -ExecutionPolicy Bypass -File <path>\hook-entry.ps1 <backend> <event>) via platform.hook_command. Copilot is the exception: its hook entry carries both a bash field (runs hook-entry.sh, macOS/Linux) and a powershell field (runs hook-entry.ps1, Windows). Copilot runs the powershell field's string itself under pwsh 7+ (not the 5.1 floor the other agents target), so the installer emits a bare & '<path>\hook-entry.ps1' <backend> <event> invocation rather than reusing platform.hook_command.
The agent-neutral pipeline that, given a normalised proposal, decides whether to show a preview, computes the original and proposed file content, and makes the RPC call into the running Neovim. Lives in-process at lua/code-preview/pre_tool/init.lua and lua/code-preview/post_tool.lua, invoked through a single RPC call from the per-agent hook entry. The historical out-of-process bash implementation (bin/core-pre-tool.sh, bin/core-post-tool.sh) was removed when issue #47 phase 3 finished for all backends; see ADR-0005 for the canonical history.
The core handler is where shell-write detection, visible_only gating, and permissionDecision emission live — everything that doesn't depend on which agent fired the hook.
The in-process Lua entry point that receives an RPC call and invokes the target function. Exported as M.dispatch(mod, fn, args_file) in lua/code-preview/rpc.lua. Reads the JSON args file written by nvim-call.sh, decodes it, looks up require(mod)[fn], and calls it.
The dispatcher is the only place user-controlled data crosses the bash/Lua boundary, and it never enters a Lua source string — that property is the whole reason the dispatcher exists (see issue #47 phase 2).
One entry in the changes registry (lua/code-preview/changes.lua): {absolute_path → status}. A change records that the agent has recently touched the file; it does not imply a preview is open. Bash writes set changes without ever producing a preview.
A change is set by the core handler before the proposal is shown, and cleared by the post-tool handler once the agent reports the proposal is done (accepted or rejected).
The value side of a change. The five recognised statuses:
modified— Edit / Write / MultiEdit / ApplyPatch on an existing file.created— same tools, on a path that didn't exist beforehand.deleted— explicit deletion (*** Delete File:in a patch, orrmdetected in a Bash command).bash_modified— Bash write detected against an existing file.bash_created— Bash write detected against a path that didn't exist.
The bash_ prefix is an origin prefix — see Origin prefix.
A convention on status values that records which kind of agent action produced the change. Today only bash_* is prefixed; un-prefixed statuses (modified, created, deleted) come from structured editing tools (Edit/Write/MultiEdit/ApplyPatch) or from explicit rm detection.
Origin prefixes exist because some agents (observed with GPT-class models in Codex) route file edits through Bash, which the plugin can't safely preview. Those proposals degrade to indicator-only (Tier 1), and the prefix lets the Bash post-tool clear its own markers without clobbering markers from a concurrent structured proposal. See ADR-0001.
Two levels of fidelity for handling Bash proposals.
- Tier 1 — implemented today. Static parsing of the shell command for redirections (
>,>>), atomic-replace (mv X.tmp X),cp,tee,sed -i, and other in-place editors (perl -i,ruby -i,gawk -i inplace) targets — plus PowerShell equivalents on Windows (Remove-Item,Set-Content,Out-File,Move-Item,Copy-Item). Implemented inpre_tool/shell_detect.lua. Sets a change with abash_*origin prefix; does not open a preview. The user sees the file was touched via the neo-tree indicator but reviews the actual content via their normal diff workflow after the fact. - Tier 2 — not implemented. Would compute and display real content diffs for shell-writes. Open design question; sandboxing was rejected (see ADR-0001). The name exists so deferred work has a label, not a commitment.
Three distinct path concepts used together in the preview pipeline. They are not interchangeable; the current code muddles them (see issue #55).
- Source path — a temp file holding pre-rendered content (
/tmp/code-preview-diff-{original,proposed}-<id>). One pair per preview:original_source_pathandproposed_source_path. Scratch files, not the real file. - File path — the absolute canonical path of the real file being edited. The identity: used as the key in
active_diffs, passed to the changes registry, used by neo-tree reveal. - Display path — what's rendered in the winbar. Usually cwd-relative for readability; never used as an identity.
When in doubt: identity = file path; content = source path; UI label = display path.
Opt-in restriction (diff.visible_only config, default false) that suppresses previews for files not currently visible in any Neovim window. The core handler asks the running Neovim via hook_context() whether the target file is in any window's visible buffer; if visible_only is on and the file isn't visible, the preview is skipped entirely (no diff tab, no inline buffer). Change indicators in neo-tree still fire — visible-only mode is about avoiding modal interruption, not about hiding that the edit happened.
Toggled at runtime via :CodePreviewToggleVisibleOnly.
The pause window between an agent firing the pre-tool hook and the agent actually writing the file — the moment during which the preview is on screen and the user can accept or reject. Every supported agent has a review gate; the mechanism differs per integration:
- Claude Code — the plugin emits
permissionDecision: "ask"in the pre-tool hook output, which forces Claude Code to prompt. Suppressible withdiff.defer_claude_permissions = true, which delegates to Claude Code's own permission settings (bypass, ask, allowlist). See ADR-0002 for why the default forces the gate. - OpenCode — gated through OpenCode's plugin API.
- Codex / Copilot CLI — relies on the agent's native ask-before-write loop.
The plugin doesn't implement the gate; it lives inside the agent. The plugin's job is to make sure the gate fires (Claude Code's case) and to render a useful preview during the gate.
The user-facing config value (diff.layout) that selects how a preview is rendered. Three values: "tab", "vsplit", "inline". The first two share one renderer; "inline" uses the other.
The internal rendering path. Two of them:
- Side-by-side renderer — opens a CURRENT and a PROPOSED buffer in two windows, uses Neovim's built-in
:diffthis. Used by bothtabandvsplitlayouts (the only difference is whether the windows live in a new tab or a vsplit of the current one). The legacy default. - Inline renderer — single buffer showing a unified-diff view, with character-level highlights,
]c/[cnavigation, and a custom statuscolumn displaying old|new line numbers. Implemented inbuild_inline_diff+inline_statuscolumninlua/code-preview/diff.lua.
The inline renderer is the strategic direction — see ADR-0003. The side-by-side renderer is kept available but is no longer where new rendering features land.
The behaviour that scrolls neo-tree to the file touched by a proposal, so the user can see the change indicator in context. Config: neo_tree.reveal (boolean, default on), neo_tree.reveal_root ("cwd" or "git" — which root neo-tree opens from).
Implementation lives in lua/code-preview/neo_tree.lua; the core handler and diff.show_diff call reveal / reveal_deferred after marking the change. The deferred variant exists because neo-tree needs a moment to settle after window changes.
Reveal target — the path neo-tree is asked to scroll to. For modified/deleted statuses it's the file itself. For created, the file doesn't yet exist on disk, so the target falls back to the nearest existing ancestor directory (or a sibling within it) — neo-tree can't highlight a path that isn't in its tree.
Precedence rule: when a Bash command both deletes and writes (rm a && echo x > b), the rm-driven reveal wins.
An RPC call the core handler issues to the running Neovim early in every hook invocation, to read config + transient state in a single hop. Two main call sites:
code-preview.log.state— returns{debug, log_file}. The shell handler uses this to decide whether to emit debug log lines and where.code-preview.hook_context(file_path)— returns{neo_tree_reveal, reveal_root, visible_only, file_visible, defer_claude_permissions, debug, log_file}. The transient bit isfile_visible: whetherfile_pathis currently shown in any visible window (only computed when visible-only mode is on).
The pattern exists because the bash layer holds no config of its own — see ADR-0004. If Neovim is unreachable, the hook degrades safely (no logging, no review gate, no visibility filter).
After issue #47 phase 3, the hook context query collapses into a local function call inside the in-process core handler; the RPC form survives only for callers that still live outside the user's Neovim (e.g. a backend that hasn't yet flipped to the Lua entry point).
A short-lived Neovim spawned with nvim --headless -l <script>.lua to do work outside the user's running Neovim. Headless workers have no UI, no access to the user's M.config or open buffers, and communicate via stdin / stdout / exit code or via RPC back to the user's instance.
Today's headless workers:
bin/apply-edit.lua— computes the proposed content for anEditproposal.bin/apply-multi-edit.lua— same forMultiEdit.bin/apply-patch.lua— parses the custom patch format and emits per-file orig/prop tempfiles forApplyPatch.
Issue #47 phases 3 and 4 do not add the core handler to this list. After an early design pass we chose to fold the handler into in-process Lua instead of a new headless worker, to eliminate the per-proposal cold-start and the chain of small RPC calls back into the user's Neovim. See ADR-0005. After phases 3/4 land, "bash core handler" goes away and the apply-* scripts remain the canonical examples of headless workers.
The plugin has two Lua code categories with different rules.
- In-process Lua — runs inside the user's running Neovim. Has access to
M.config, open buffers, windows, autocmds, neo-tree, the changes registry. Lives underlua/code-preview/. Examples:init.lua,diff.lua,rpc.lua'sdispatch,pidfile.lua'ssetup. - Headless Lua — runs in a headless worker. No access to the user's session. Must communicate findings back via stdout, tempfiles, or RPC. Lives under
bin/. Examples:apply-edit.lua,apply-patch.lua.
When deciding where new code belongs: anything that needs to see or change the user's Neovim state is in-process; anything that just transforms data is a candidate for a headless worker, and after #47 phases 3/4 the orchestration around it lives in a headless worker too.
The rendered icon and highlight in the neo-tree filesystem source for a file that has a change. One indicator per change. The mapping from status → glyph + highlight is configured under neo_tree.symbols and neo_tree.highlights. The indicator is the visual; the change is the data.