overflow/fs-review.nvim

github github
ai
stars 2
issues 0
subscribers 0
forks 0
CREATED

UPDATED


fs-review.nvim

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.


What it is

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:

  • External changes โ€” every file the filesystem watcher sees (your editor, another tool, an agent, a coworker in another IDE).
  • OpenCode activity โ€” every file OpenCode sessions read or modified for the current project directory, reconciled against the live SSE event stream.

Press <CR> on any line to open an isolated diff tab (HEAD vs. worktree) without losing your editor layout. Press q to come back.


Features

  • ๐Ÿ“‚ Recursive filesystem watcher powered by vim.uv.fs_event.
  • ๐Ÿ”€ Git diff view โ€” tracks git diff --numstat per file in real time.
  • ๐Ÿค– OpenCode activity view โ€” parses /session, /session/:id/message and the SSE /api/event stream to surface files the agent touched.
  • ๐ŸชŸ Isolated review tabs โ€” HEAD vs. worktree diff in a dedicated tab that survives closing the panel.
  • ๐Ÿ“ก Project-scoped OpenCode server โ€” auto_serve boots opencode serve --port <port> if the local server isn't already up.
  • ๐Ÿงญ Current activity row โ€” shows the most recent read/modified action.
  • ๐Ÿ”„ Reconciliation across processes โ€” a periodic signature probe compares session state so refreshes survive process restarts.
  • ๐ŸŒ EN / ES โ€” every visible string is overridable.
  • ๐ŸŽจ Theme-aware highlights โ€” Title, Comment, DiagnosticInfo, Special namespaces.

Requirements

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.


Installation

LazyVim (recommended)

Add the plugin under lua/plugins/fs-review.lua:

return {
  "overflow/fs-review.nvim",
  lazy = false,
  priority = 1000,
  opts = {}, -- see Configuration
}

lazy.nvim (manual)

{
  "overflow/fs-review.nvim",
  lazy = false,
  priority = 1000,
  config = function()
    require("fs_review").setup({})
  end,
}

mini.deps

MiniDeps.add("overflow/fs-review.nvim")
vim.schedule(function() require("fs_review").setup({}) end)

packer.nvim (deprecated)

use {
  "overflow/fs-review.nvim",
  config = function() require("fs_review").setup({}) end,
}

Tip: if you use :Lazy to install, make sure the plugin folder is on runtimepath before Neovim reads ~/.config/nvim/init.lua.


Configuration

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.


Commands and mappings

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.


Usage walkthrough

  1. :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.

  2. Edit src/a.js outside Neovim (or trigger :!touch src/a.js from another shell). The row pulses with a fresh timestamp.

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

  4. 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โ€ฆ]
    
  5. 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.


Architecture

State machine

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()

SSE + reconciliation policy

  • start_event_stream(base, target) opens an SSE curl -N job against base .. "/api/event" with header x-opencode-directory: <target>.
  • Each 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.
  • A monotonic 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.

Isolated review tabs

  • open_review(path) calls ensure_review_layout() which records the caller's tab/window, opens tabnew, then vsplit for the worktree side.
  • Buffers are 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.


Troubleshooting

"Unable to connect to the local OpenCode server"

The plugin probes with curl --max-time 1 then falls back to a /dev/tcp connect. If both fail:

  • Confirm opencode serve --port 4096 works in your shell.
  • Check vim.g.opencode_api_url is not set to an external host.
  • Verify the project directory you opened contains a .git/ folder (the plugin uses it as the project root).

Sessions are not appearing

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.

Wrong project directory

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.

Panels persist after :bd

Each 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).

OpenCode server split (TUI vs 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.

Headless validation snippet

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

Roadmap

  • Monorepo activity: per-package scope (anchor by package.json / pyproject.toml / go.mod etc.).
  • Custom scopes: pass a closure that resolves a project root from vim.api.nvim_buf_get_name(0).
  • Telemetry export to oil.nvim-style floats.

Open a feature request if you want something on this list.


Contributing

See CONTRIBUTING.md for the Conventional Commits table, the PR template, and the napkin rule about light autocommand callbacks.


License

MIT ยฉ 2026 overflow.

Acknowledgements

  • OpenCode โ€” local-first coding agent.
  • Oil โ€” inspiration for the file-tree-style sidebar.
  • LazyVim โ€” the distribution this plugin was built for.