A filesystem + OpenCode review panel for LazyVim โ track what changed on disk and what the agent is touching, then open an isolated diff in one keystroke.
fs-review.nvim is a LazyVim-friendly Neovim plugin that gives you a single
floating panel to review what is happening on disk and what your local
OpenCode agent is doing in the current project.
It has two modes, both visible in the same panel:
Press <CR> on any line to open an isolated diff tab (HEAD vs.
worktree) without losing your editor layout. Press q to come back.
vim.uv.fs_event.git diff --numstat per file in real time./session, /session/:id/message
and the SSE /api/event stream to surface files the agent touched.HEAD vs. worktree diff in a dedicated tab
that survives closing the panel.auto_serve boots
opencode serve --port <port> if the local server isn't already up.Title, Comment, DiagnosticInfo,
Special namespaces.| Tool | Version | Notes |
|---|---|---|
| Neovim | โฅ 0.10 | requires vim.uv, vim.system, vim.uri_encode |
| LazyVim | any recent | spec provided; works standalone too |
curl |
any | used for HTTP + SSE probes |
git |
โฅ 2.20 | uses git diff --numstat and git show HEAD: |
opencode CLI |
โฅ 1.18 | optional, only needed for OpenCode activity |
All requirements are present on a stock LazyVim install except opencode,
which the plugin will offer to install / boot on first use.
Add the plugin under lua/plugins/fs-review.lua:
return {
"overflow/fs-review.nvim",
lazy = false,
priority = 1000,
opts = {}, -- see Configuration
}
{
"overflow/fs-review.nvim",
lazy = false,
priority = 1000,
config = function()
require("fs_review").setup({})
end,
}
MiniDeps.add("overflow/fs-review.nvim")
vim.schedule(function() require("fs_review").setup({}) end)
use {
"overflow/fs-review.nvim",
config = function() require("fs_review").setup({}) end,
}
Tip: if you use
:Lazyto install, make sure the plugin folder is onruntimepathbefore Neovim reads~/.config/nvim/init.lua.
Default options (override via require('fs_review').setup({...})):
require("fs_review").setup({
language = "en", -- "en" | "es"
auto_serve = true, -- boot `opencode serve` when missing
opencode_port = nil, -- nil โ 4096; respects vim.g.opencode_opts.server.port
opencode_reconcile_interval = 2500, -- ms between SSE-friendly reconciliation polls
strings = { -- any key overrides a locale string
en = {
-- full list: see lua/fs_review/init.lua
-- e.g. activity = "Agent activity",
},
es = {
-- e.g. activity = "Actividad del agente",
},
},
})
| Option | Default | Description |
|---|---|---|
language |
"en" |
Locale for all visible strings. Falls back to "en" if missing. |
auto_serve |
true |
When the local OpenCode server is unreachable, run opencode serve --port <port> from the project root. |
opencode_port |
nil |
Explicit port. nil reads vim.g.opencode_opts.server.port, then falls back to 4096. |
opencode_reconcile_interval |
2500 |
Milliseconds between reconciliation probes while the panel is open. 0 disables reconciliation. |
strings.en.* / strings.es.* |
{} |
Per-locale overrides. Keys map 1:1 to defaults (see source). |
The per-call API is require('fs_review').setup({}). Calling setup again
is safe: it cleans up watchers, timers and jobs before reloading.
| Command | Mode | Description | Default panel keymap |
|---|---|---|---|
:FsWatchRestart |
git | Restart the recursive filesystem watcher from the current project root. | โ |
:FsReviewGit |
git | Toggle the Git filesystem review panel (and flip into git mode if needed). | โ |
:FsReviewToggle |
git | Alias for :FsReviewGit. |
โ |
:FsReviewOpenCode |
opencode | Query /session for the current project and switch the panel into OpenCode activity mode. |
โ |
:FsReviewOpenCodeToggle |
opencode | Toggle the OpenCode activity panel. | โ |
:FsReviewOpenCodeStop |
opencode | Stop the local opencode serve job and the SSE stream. |
โ |
:FsReviewShow [{path}] |
both | Open an isolated diff tab for {path} (defaults to the first entry). |
<CR> on a row |
:FsReviewClear |
both | Empty the in-memory review queue for the active mode. | โ |
:FsReviewCloseAll |
both | Close the floating panel and the isolated review tab. | q on the panel |
Inside the panel:
<CR> โ open the isolated diff tab for the entry under the cursor.d โ same as <CR> but also close the panel.q โ close the panel.Inside the review tab:
q โ close the review tab and return to the original layout.Mappings are buffer-local; they do not leak into other buffers.
:FsReviewGit opens a 60-column panel at the bottom listing every file
the watcher has seen since startup. The header reads
External changes (N) โ <CR> view diff ยท q close.
Edit src/a.js outside Neovim (or trigger :!touch src/a.js from
another shell). The row pulses with a fresh timestamp.
Move the cursor onto src/a.js and press <CR>. A new tab opens with
[FS Review HEAD] and [FS Review WORKTREE] buffers side-by-side, diff
mode enabled.
Back in the original tab, run :FsReviewOpenCode. The panel flips to
OpenCode activity:
OpenCode activity โ Connected ยท updated just now โ <CR> modified diff ยท q close
Current activity:
Completed modification ยท src/a.js ยท just now
Modified (3):
src/a.js modified ร2 ยท just now [sess_8xโฆ]
src/b.js modified ร1 ยท 5m ago [sess_8xโฆ]
docs/index.md modified ร1 ยท 12m ago [sess_q1โฆ]
After the OpenCode server runs for a while the reconciliation probe detects session signature drift (e.g. a sibling session in another shell wrote to a file) and re-runs the query automatically.
Press :FsReviewCloseAll to return to your original layout.
The plugin has two modes:
git โ passive. Watches the filesystem, snapshots files into
state.git_files, polls git diff --numstat on render.opencode โ active. Boots the SSE event stream, runs /session queries,
parses per-session messages, reconciles via a signature probe. setup() FsReviewGit
idle โโโโโโโโโโโโโโโบ git mode โโโโโโโโโโโโโโโ
โ โ
start_watching() โ
โ โ
โผ โ
panel_open โโโโโโโโโโโโโโโโโ
โ
FsReviewOpenCodeToggle
โผ
opencode mode
โ
load_opencode() โ query โ stream
โ
event_force_stopped?
โโโโโโโโโดโโโโโโโโโ
yes no
โ โ
cleanup() start_reconciliation()
start_event_stream(base, target) opens an SSE curl -N job against
base .. "/api/event" with header x-opencode-directory: <target>.data: line triggers schedule_opencode_refresh(target) (debounced
to 2 seconds).start_reconciliation() runs vim.uv.new_timer() every
opencode_reconcile_interval ms. The probe computes a signature of
(id, updated_at_kind, updated_at) for every selected session and calls
load_opencode(target) only when the signature differs from the cached
one.opencode_request token invalidates stale callbacks; a
reconciliation_probe_token does the same for the probe job. If the
probe fails, the next tick retries without flooding the user.open_review(path) calls ensure_review_layout() which records the
caller's tab/window, opens tabnew, then vsplit for the worktree side.nofile, bufhidden=wipe, swapfile=false, and get a
stable filename (fs-review://HEAD/<rel> and fs-review://WORKTREE/<rel>)
so they survive BufWipeout cycles.close_review() restores the caller's tab/window via the stored
review_caller_tab / review_caller_win pair.See docs/architecture.md for diagrams.
The plugin probes with curl --max-time 1 then falls back to a
/dev/tcp connect. If both fail:
opencode serve --port 4096 works in your shell.vim.g.opencode_api_url is not set to an external host..git/ folder
(the plugin uses it as the project root).OpenCode scopes sessions by the directory the agent was launched in. The
plugin only shows sessions whose directory (or cwd) equals
find_project_root() or sits under it. If you started the agent from a
worktree or sub-directory, the session won't show up under a parent repo
โ launch the agent from the project root, or open the worktree in a
separate Neovim instance.
The plugin walks parents looking for .git/. If you have a monorepo with
nested .git/ markers, anchor your session with vim.g.opencode_opts.server.port
or open the right subfolder.
:bdEach panel registers a BufWipeout autocmd that calls
invalidate_opencode_load() and stop_reconciliation(). If you closed
the buffer with bwipe! and timers keep running, make sure the autocmd
fired (check :autocmd FsReview) โ the autocmd uses the modern
{ buffer = ..., callback = function(args) ... end } shape; do not pass
a positional match argument or the callback will receive the wrong
arguments and silently fail (this is the Napkin rule).
serve)OpenCode runs in two distinct modes:
opencode (TUI) โ interactive shell. Does not expose the HTTP API.opencode serve --port <port> โ background server exposing
/session, /session/:id/message and the /api/event SSE stream.The plugin only talks to the serve mode. If you started OpenCode from
the TUI, restart it with opencode serve --port 4096 & from your
project root.
nvim --headless -u NONE \
--cmd "set rtp+=/home/overflow/Code/fs-review.nvim" \
-c "lua require('fs_review').setup({ auto_serve = false })" \
-c "lua vim.cmd('FsReviewGit')" \
-c "qa!" \
&& echo OK
package.json /
pyproject.toml / go.mod etc.).vim.api.nvim_buf_get_name(0).oil.nvim-style floats.Open a feature request if you want something on this list.
See CONTRIBUTING.md for the Conventional Commits table,
the PR template, and the napkin rule about light autocommand callbacks.
MIT ยฉ 2026 overflow.