opencode-nvim-diff-review 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Richards
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # nvim-diff-review-opencode-plugin
2
+
3
+ An agent-driven guided code review tool for Neovim and [OpenCode](https://opencode.ai). An AI agent walks you through code changes file by file, showing side-by-side diffs in Neovim while discussing the changes in a separate OpenCode chat.
4
+
5
+ ## How it works
6
+
7
+ The plugin connects an OpenCode agent to Neovim's [diffview.nvim](https://github.com/sindrets/diffview.nvim) via Neovim's built-in RPC socket. The agent can open diffs, navigate between files, and query the current review state — all while the user interacts via the OpenCode chat.
8
+
9
+ ```
10
+ ┌────────────────────────────┬──────────────────────┐
11
+ │ │ │
12
+ │ Neovim showing diff of │ OpenCode agent: │
13
+ │ src/store/selectors.js │ │
14
+ │ │ "I refactored the │
15
+ │ - old code in red │ getBlockName func │
16
+ │ + new code in green │ to handle the new │
17
+ │ │ edge case where..." │
18
+ │ │ │
19
+ │ │ Any questions about │
20
+ │ │ these changes? │
21
+ └────────────────────────────┴──────────────────────┘
22
+ ```
23
+
24
+ The review workflow:
25
+
26
+ 1. Agent opens a diff view in Neovim (scoped to specific files or all uncommitted changes)
27
+ 2. Agent explains the changes in the current file
28
+ 3. User asks questions or leaves feedback — agent notes it but **does not edit files**
29
+ 4. Agent navigates to the next file and repeats
30
+ 5. After the last file, agent closes the diff view
31
+ 6. Agent proposes a commit message and commits the original work
32
+ 7. If feedback was left, agent applies it as a separate commit
33
+ 8. Optionally, a second review of just the feedback changes
34
+
35
+ ## Components
36
+
37
+ The plugin has two parts that are installed separately:
38
+
39
+ ### 1. Neovim plugin (`lua/diff-review/`)
40
+
41
+ A Lua module that:
42
+
43
+ - Registers a global `DiffviewState()` function queryable via Neovim's RPC socket
44
+ - Provides diffview.nvim hooks that clean up buffers when the diff view is closed (so reviewed files don't linger as open tabs)
45
+
46
+ ### 2. OpenCode plugin (`opencode-plugin/index.ts`)
47
+
48
+ An OpenCode plugin that registers a `diff_review` tool the AI agent uses to control the diff view. Actions:
49
+
50
+ | Action | Description |
51
+ |--------|-------------|
52
+ | `open` | Open a diff view, optionally scoped to specific files or a git ref. Returns the file list and current position. |
53
+ | `next` | Navigate to the next file. Detects and prevents wrap-around at the last file. |
54
+ | `prev` | Navigate to the previous file. Detects and prevents wrap-around at the first file. |
55
+ | `status` | Get current file and position without navigating. Includes the full file list. |
56
+ | `close` | Close the diff view and clean up buffers. |
57
+
58
+ ## Dependencies
59
+
60
+ - [Neovim](https://neovim.io/) 0.9+
61
+ - [diffview.nvim](https://github.com/sindrets/diffview.nvim)
62
+ - [OpenCode](https://opencode.ai) 1.3+
63
+
64
+ ## Installation
65
+
66
+ ### 1. Neovim plugin
67
+
68
+ Using [lazy.nvim](https://github.com/folke/lazy.nvim):
69
+
70
+ ```lua
71
+ {
72
+ "talldan/nvim-diff-review-opencode-plugin",
73
+ dependencies = { "sindrets/diffview.nvim" },
74
+ config = function()
75
+ require("diff-review").setup()
76
+ end,
77
+ }
78
+ ```
79
+
80
+ This also installs diffview.nvim as a dependency if you don't already have it. You can configure diffview.nvim separately — the plugin merges its hooks with any existing diffview hooks you have.
81
+
82
+ ### 2. OpenCode plugin
83
+
84
+ Add the plugin to your `opencode.json` configuration:
85
+
86
+ ```json
87
+ {
88
+ "plugin": ["github:talldan/nvim-diff-review-opencode-plugin"]
89
+ }
90
+ ```
91
+
92
+ This can go in your global config (`~/.config/opencode/opencode.json`) or a project-level config (`opencode.json` in your project root).
93
+
94
+ Restart OpenCode to load the plugin. The `diff_review` tool will be available to the AI agent automatically.
95
+
96
+ ### 3. Neovim RPC socket
97
+
98
+ The tool communicates with Neovim via its RPC socket. You need to:
99
+
100
+ 1. Start Neovim with a listen address:
101
+ ```bash
102
+ export NVIM_SOCKET=/tmp/nvim.sock
103
+ nvim --listen $NVIM_SOCKET
104
+ ```
105
+
106
+ 2. Make sure `NVIM_SOCKET` is set in the environment where OpenCode runs.
107
+
108
+ If you use [CMUX](https://cmux.com), you can set this in your workspace configuration so both Neovim and OpenCode share the socket path automatically.
109
+
110
+ ## Design
111
+
112
+ ### Architecture
113
+
114
+ ```
115
+ OpenCode (agent) Neovim (editor)
116
+ │ │
117
+ │ nvim --headless --server │
118
+ │ $NVIM_SOCKET --remote-expr │
119
+ │ "luaeval('...')" │
120
+ │ ─────────────────────────────────>│
121
+ │ │
122
+ │ JSON response │
123
+ │ <─────────────────────────────────│
124
+ │ │
125
+ ```
126
+
127
+ The tool uses Neovim's `--remote-expr` to evaluate Lua expressions on the running Neovim instance. This is a standard Neovim feature that works in any terminal — no CMUX or specific terminal emulator required.
128
+
129
+ Key design decisions:
130
+
131
+ - **`--headless` flag on remote calls**: Prevents Neovim from emitting terminal escape sequences when invoked from a subprocess (e.g., Bun's shell). Without this, the JSON response gets polluted with control codes.
132
+ - **State queries via `DiffviewState()`**: A global Lua function registered at plugin load time. Returns JSON with the current file, position, and full file list. Registered as a global (not module-scoped) so it can be called via `luaeval()` without needing the module require path.
133
+ - **Wrap-around prevention**: diffview.nvim wraps from the last file to the first (and vice versa) when navigating. The tool detects this by comparing indices before and after navigation, and undoes the wrap if detected.
134
+ - **Buffer cleanup on close**: diffview.nvim intentionally keeps local file buffers open after closing (so you can continue editing). The plugin tracks which buffers existed before the review and removes any new ones on close — unless they have unsaved edits.
135
+ - **Small delays after navigation**: 200-500ms sleeps after diffview commands to let the UI update before querying state. Without this, the state query can return stale data.
136
+
137
+ ### Review workflow instructions
138
+
139
+ The tool description embeds detailed workflow instructions for the AI agent:
140
+
141
+ - **Lint before review**: The agent is told to fix lint/format issues before opening the diff, so the user only sees clean changes.
142
+ - **No edits during review**: The agent is explicitly instructed to never edit files during the review. Feedback is collected and applied afterward.
143
+ - **Two-commit pattern**: Original work is committed first, then feedback changes are committed separately. This gives clean git history and allows the second review to show only the feedback diff.
144
+ - **Interactive pacing**: The agent explains each file's changes, asks for feedback, and waits for the user's response before moving on.
145
+
146
+ ## Future improvements
147
+
148
+ These were discussed during development but not yet implemented:
149
+
150
+ ### Chunk-level navigation
151
+
152
+ Navigate within a file between individual hunks (like `git add -p`), not just between files. This would allow the agent to walk through a large file's changes piece by piece rather than presenting the whole diff at once.
153
+
154
+ ### Logical ordering
155
+
156
+ Instead of reviewing files in filesystem order, the agent would use its understanding of the codebase to present changes in a narrative order — e.g., "first the data model change, then the API that uses it, then the UI that calls the API." This would make reviews of larger changesets more coherent.
157
+
158
+ ### Accept/reject per-hunk
159
+
160
+ Allow the user to accept or reject individual hunks from the diff view, similar to `git add -p`. This would integrate with diffview.nvim's staging capabilities.
161
+
162
+ ### Line range focus
163
+
164
+ The agent could jump to specific line ranges within a diff to highlight the key change, rather than showing the full file diff. Useful for large files where only a small section was modified.
165
+
166
+ ### OpenCode session diff integration
167
+
168
+ Instead of using `git diff` to determine changed files, use OpenCode's `/session/:id/diff` API endpoint to get exactly which files the agent modified in the current session. This would avoid showing unrelated uncommitted changes.
@@ -0,0 +1,154 @@
1
+ -- diff-review: Neovim plugin for OpenCode-driven guided code reviews
2
+ --
3
+ -- Provides:
4
+ -- 1. A global DiffviewState() function that external tools can query via Neovim's
5
+ -- RPC socket to get the current diffview state (file, position, file list).
6
+ -- 2. Diffview hooks that clean up buffers when the diff view is closed.
7
+ --
8
+ -- Requires: sindrets/diffview.nvim
9
+ --
10
+ -- Usage from outside Neovim:
11
+ -- nvim --headless --server $NVIM_SOCKET --remote-expr "luaeval('DiffviewState()')"
12
+
13
+ local M = {}
14
+
15
+ --- Track which buffers existed before diffview opened.
16
+ --- Used by the cleanup hooks to avoid removing buffers the user had open.
17
+ local pre_diffview_bufs = {}
18
+
19
+ --- Query the current diffview state.
20
+ --- Returns a JSON string with:
21
+ --- open: boolean - whether diffview is active
22
+ --- current_file: string? - repo-relative path of the file being shown
23
+ --- absolute_path: string? - absolute path on disk
24
+ --- status: string? - git status letter (M, A, D, R, etc.)
25
+ --- index: number? - 1-based position in the file list
26
+ --- total: number? - total number of changed files
27
+ --- files: table? - array of {path, status} for all files in the diff
28
+ ---
29
+ --- Registered as a global function so it can be called via luaeval() from
30
+ --- Neovim's --remote-expr without requiring the module path.
31
+ function DiffviewState()
32
+ local ok, lib = pcall(require, "diffview.lib")
33
+ if not ok then
34
+ return vim.json.encode({ open = false, error = "diffview.nvim not loaded" })
35
+ end
36
+
37
+ local utils = require("diffview.utils")
38
+ local view = lib.get_current_view()
39
+ if not view then
40
+ return vim.json.encode({ open = false })
41
+ end
42
+
43
+ local panel = view.panel
44
+ local cur_file = panel.cur_file
45
+ local files = panel:ordered_file_list()
46
+ local total = #files
47
+
48
+ if not cur_file then
49
+ return vim.json.encode({ open = true, current_file = vim.NIL, index = 0, total = total })
50
+ end
51
+
52
+ local index = utils.vec_indexof(files, cur_file)
53
+
54
+ -- Build the list of all file paths for the summary
55
+ local all_files = {}
56
+ for _, f in ipairs(files) do
57
+ table.insert(all_files, { path = f.path, status = f.status })
58
+ end
59
+
60
+ return vim.json.encode({
61
+ open = true,
62
+ current_file = cur_file.path,
63
+ absolute_path = cur_file.absolute_path,
64
+ status = cur_file.status,
65
+ index = index,
66
+ total = total,
67
+ files = all_files,
68
+ })
69
+ end
70
+
71
+ --- Diffview hook: called when a diff view is opened.
72
+ --- Snapshots the current buffer list so we know which buffers to clean up later.
73
+ function M.on_view_opened(view)
74
+ pre_diffview_bufs = {}
75
+ for _, buf in ipairs(vim.api.nvim_list_bufs()) do
76
+ if vim.api.nvim_buf_is_loaded(buf) then
77
+ pre_diffview_bufs[buf] = true
78
+ end
79
+ end
80
+ end
81
+
82
+ --- Diffview hook: called when a diff view is closed.
83
+ --- Cleans up buffers that diffview created but the user didn't have open before.
84
+ --- - diffview:// internal buffers are always removed
85
+ --- - Real file buffers opened by diffview are removed if they have no unsaved edits
86
+ function M.on_view_closed(view)
87
+ vim.schedule(function()
88
+ for _, buf in ipairs(vim.api.nvim_list_bufs()) do
89
+ if vim.api.nvim_buf_is_valid(buf) and not pre_diffview_bufs[buf] then
90
+ local name = vim.api.nvim_buf_get_name(buf)
91
+ -- Always clean up diffview's internal buffers (diffview:// scheme)
92
+ if name:match("^diffview://") then
93
+ pcall(vim.api.nvim_buf_delete, buf, { force = true })
94
+ -- Also clean up real file buffers that diffview opened, but only if
95
+ -- they're unmodified (don't wipe user edits)
96
+ elseif vim.api.nvim_buf_is_loaded(buf) and not vim.bo[buf].modified then
97
+ pcall(vim.api.nvim_buf_delete, buf, {})
98
+ end
99
+ end
100
+ end
101
+ pre_diffview_bufs = {}
102
+ end)
103
+ end
104
+
105
+ --- Set up the plugin. Call this from your plugin spec or init.lua.
106
+ --- Configures diffview.nvim hooks for buffer cleanup.
107
+ ---
108
+ --- Example with lazy.nvim:
109
+ --- {
110
+ --- "your-username/nvim-diff-review-opencode-plugin",
111
+ --- dependencies = { "sindrets/diffview.nvim" },
112
+ --- config = function()
113
+ --- require("diff-review").setup()
114
+ --- end,
115
+ --- }
116
+ function M.setup(opts)
117
+ opts = opts or {}
118
+
119
+ -- Register the DiffviewState global function (already done at module load,
120
+ -- but this ensures it's available even if the module is lazy-loaded)
121
+ _G.DiffviewState = DiffviewState
122
+
123
+ -- Configure diffview hooks
124
+ local dv_ok, diffview = pcall(require, "diffview")
125
+ if not dv_ok then
126
+ vim.notify(
127
+ "[diff-review] diffview.nvim is required but not installed",
128
+ vim.log.levels.WARN
129
+ )
130
+ return
131
+ end
132
+
133
+ -- Get existing diffview config and merge our hooks
134
+ local config = require("diffview.config")
135
+ local existing_hooks = config.get_config().hooks or {}
136
+
137
+ local orig_view_opened = existing_hooks.view_opened
138
+ local orig_view_closed = existing_hooks.view_closed
139
+
140
+ diffview.setup({
141
+ hooks = vim.tbl_extend("force", existing_hooks, {
142
+ view_opened = function(view)
143
+ M.on_view_opened(view)
144
+ if orig_view_opened then orig_view_opened(view) end
145
+ end,
146
+ view_closed = function(view)
147
+ M.on_view_closed(view)
148
+ if orig_view_closed then orig_view_closed(view) end
149
+ end,
150
+ }),
151
+ })
152
+ end
153
+
154
+ return M
@@ -0,0 +1,225 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+
3
+ interface DiffviewFileInfo {
4
+ path: string
5
+ status: string
6
+ }
7
+
8
+ interface DiffviewState {
9
+ open: boolean
10
+ current_file?: string | null
11
+ absolute_path?: string
12
+ status?: string
13
+ index?: number
14
+ total?: number
15
+ files?: DiffviewFileInfo[]
16
+ error?: string
17
+ }
18
+
19
+ export const DiffReviewPlugin: Plugin = async (ctx) => {
20
+ return {
21
+ tool: {
22
+ diff_review: tool({
23
+ description:
24
+ "Control a diff review view in the user's Neovim editor. Use this to walk " +
25
+ "the user through code changes after completing a task.\n\n" +
26
+ "IMPORTANT: Before starting a review, ensure all changes are clean:\n" +
27
+ "- Run any relevant linters/formatters and fix issues BEFORE opening the diff\n" +
28
+ "- The user should only see final, clean changes — not intermediate lint fixes\n" +
29
+ "- If you discover lint errors during review, close the diff, fix them, then restart\n\n" +
30
+ "Workflow:\n" +
31
+ "1. Fix any lint/format issues in your changes first\n" +
32
+ "2. Call with action 'open' to show the diff (optionally scoped to specific files)\n" +
33
+ " — the response includes a list of ALL files that will be reviewed\n" +
34
+ "3. Explain the changes visible in the current file (the response tells you which file is shown)\n" +
35
+ "4. Ask the user if they have questions or feedback about these changes\n" +
36
+ "5. If the user requests changes or leaves feedback on the current file, acknowledge it\n" +
37
+ " and note it down — but DO NOT make any changes yet. Continue the review.\n" +
38
+ "6. Call with action 'next' to move to the next changed file\n" +
39
+ " — when you reach the last file, 'next' will tell you there are no more files\n" +
40
+ "7. Repeat steps 3-6 for each file\n" +
41
+ "8. Call with action 'close' when the review is complete\n" +
42
+ "9. Propose a git commit message for the CURRENT changes and commit if the user approves\n" +
43
+ "10. If the user left feedback or change requests during the review, NOW apply them\n" +
44
+ " — this creates a clean separation: one commit for the original work,\n" +
45
+ " a second commit for review feedback changes\n" +
46
+ "11. If you made feedback changes, offer to walk through them with a second diff_review\n" +
47
+ " — since the original work is already committed, this diff will only show\n" +
48
+ " the feedback changes, making them easy to verify\n\n" +
49
+ "CRITICAL: During the review (steps 3-7), NEVER make changes to files.\n" +
50
+ "Only collect feedback. Apply changes AFTER the review is closed and the\n" +
51
+ "original work is committed.\n\n" +
52
+ "Every response includes the current file path and position (e.g., '2 of 3') " +
53
+ "so you always know where you are in the review. Use the 'status' action " +
54
+ "to re-orient if you lose track.",
55
+ args: {
56
+ action: tool.schema
57
+ .enum(["open", "next", "prev", "close", "status"])
58
+ .describe(
59
+ "open: show diff view in Neovim. " +
60
+ "next: navigate to next changed file. " +
61
+ "prev: navigate to previous changed file. " +
62
+ "close: close the diff view. " +
63
+ "status: get current file and position without navigating."
64
+ ),
65
+ files: tool.schema
66
+ .array(tool.schema.string())
67
+ .optional()
68
+ .describe(
69
+ "File paths to include in the diff (open only). " +
70
+ "Omit to show all uncommitted changes."
71
+ ),
72
+ ref: tool.schema
73
+ .string()
74
+ .optional()
75
+ .describe(
76
+ "Git ref to diff against (open only). " +
77
+ "Defaults to showing uncommitted changes vs HEAD. " +
78
+ "Examples: HEAD~3, a commit hash, origin/main"
79
+ ),
80
+ },
81
+ async execute(args, context) {
82
+ const socket = process.env.NVIM_SOCKET
83
+ if (!socket) {
84
+ return "NVIM_SOCKET environment variable is not set. " +
85
+ "Make sure Neovim is running with --listen and NVIM_SOCKET is exported.\n\n" +
86
+ "Quick setup:\n" +
87
+ " export NVIM_SOCKET=/tmp/nvim.sock\n" +
88
+ " nvim --listen $NVIM_SOCKET\n\n" +
89
+ "If using CMUX, the workspace command sets this automatically."
90
+ }
91
+
92
+ const nvimExpr = (expr: string) =>
93
+ Bun.$`nvim --headless --server ${socket} --remote-expr ${expr}`.text()
94
+
95
+ const getState = async (): Promise<DiffviewState> => {
96
+ try {
97
+ const raw = await nvimExpr(`luaeval("DiffviewState()")`)
98
+ return JSON.parse(raw.trim())
99
+ } catch {
100
+ return { open: false, error: "Could not query diffview state" }
101
+ }
102
+ }
103
+
104
+ const statusLabel = (status: string | undefined): string =>
105
+ status === "M" ? "modified" :
106
+ status === "A" ? "added" :
107
+ status === "D" ? "deleted" :
108
+ status === "R" ? "renamed" :
109
+ status ?? "changed"
110
+
111
+ const formatState = (state: DiffviewState): string => {
112
+ if (!state.open) return "Diff view is not open."
113
+ if (!state.current_file)
114
+ return `Diff view is open but no files to show (${state.total ?? 0} files total).`
115
+ return `Currently showing: ${state.current_file} (${statusLabel(state.status)}) — file ${state.index} of ${state.total}.`
116
+ }
117
+
118
+ const formatFileList = (state: DiffviewState): string => {
119
+ if (!state.files || state.files.length === 0) return ""
120
+ const list = state.files
121
+ .map((f, i) => ` ${i + 1}. ${f.path} (${statusLabel(f.status)})`)
122
+ .join("\n")
123
+ return `\nFiles to review:\n${list}`
124
+ }
125
+
126
+ try {
127
+ switch (args.action) {
128
+ case "open": {
129
+ let cmd = "DiffviewOpen"
130
+ if (args.ref) {
131
+ cmd += ` ${args.ref}`
132
+ }
133
+ if (args.files && args.files.length > 0) {
134
+ const escaped = args.files.map(f => f.replace(/ /g, "\\ ")).join(" ")
135
+ cmd += ` -- ${escaped}`
136
+ }
137
+ await nvimExpr(`luaeval("vim.cmd('${cmd.replace(/'/g, "''")}')")`)
138
+ // Give diffview a moment to populate the file list
139
+ await Bun.sleep(500)
140
+ const state = await getState()
141
+ return `Opened diff view in Neovim` +
142
+ (args.ref ? ` (comparing against ${args.ref})` : " (uncommitted changes vs HEAD)") +
143
+ `. ${formatState(state)}` +
144
+ formatFileList(state)
145
+ }
146
+
147
+ case "next": {
148
+ const before = await getState()
149
+ if (!before.open) return "Diff view is not open. Call with action 'open' first."
150
+
151
+ // If already on the last file, don't navigate (diffview wraps around)
152
+ if (before.index !== undefined && before.total !== undefined &&
153
+ before.index >= before.total) {
154
+ return `Already at the last file (file ${before.index} of ${before.total}). ` +
155
+ `${formatState(before)} There are no more files to review. ` +
156
+ "Use action 'close' to end the review."
157
+ }
158
+
159
+ await nvimExpr(`luaeval("require('diffview').emit('select_next_entry')")`)
160
+ await Bun.sleep(200)
161
+ const after = await getState()
162
+
163
+ // Detect wrap-around: if index went down, diffview wrapped to the beginning
164
+ if (before.index !== undefined && after.index !== undefined &&
165
+ after.index < before.index) {
166
+ // Undo the wrap by going back
167
+ await nvimExpr(`luaeval("require('diffview').emit('select_prev_entry')")`)
168
+ await Bun.sleep(200)
169
+ const restored = await getState()
170
+ return `Already at the last file (file ${restored.index} of ${restored.total}). ` +
171
+ `${formatState(restored)} There are no more files to review. ` +
172
+ "Use action 'close' to end the review."
173
+ }
174
+
175
+ return `Navigated to next file. ${formatState(after)}`
176
+ }
177
+
178
+ case "prev": {
179
+ const before = await getState()
180
+ if (!before.open) return "Diff view is not open. Call with action 'open' first."
181
+
182
+ // If already on the first file, don't navigate (diffview wraps around)
183
+ if (before.index !== undefined && before.index <= 1) {
184
+ return `Already at the first file (file ${before.index} of ${before.total}). ` +
185
+ `${formatState(before)} There are no previous files.`
186
+ }
187
+
188
+ await nvimExpr(`luaeval("require('diffview').emit('select_prev_entry')")`)
189
+ await Bun.sleep(200)
190
+ const after = await getState()
191
+
192
+ // Detect wrap-around: if index went up, diffview wrapped to the end
193
+ if (before.index !== undefined && after.index !== undefined &&
194
+ after.index > before.index) {
195
+ // Undo the wrap by going forward
196
+ await nvimExpr(`luaeval("require('diffview').emit('select_next_entry')")`)
197
+ await Bun.sleep(200)
198
+ const restored = await getState()
199
+ return `Already at the first file (file ${restored.index} of ${restored.total}). ` +
200
+ `${formatState(restored)} There are no previous files.`
201
+ }
202
+
203
+ return `Navigated to previous file. ${formatState(after)}`
204
+ }
205
+
206
+ case "status": {
207
+ const state = await getState()
208
+ if (!state.open) return "Diff view is not currently open."
209
+ return `${formatState(state)}${formatFileList(state)}`
210
+ }
211
+
212
+ case "close": {
213
+ await nvimExpr(`luaeval("require('diffview').close()")`)
214
+ return "Closed the diff view in Neovim."
215
+ }
216
+ }
217
+ } catch (e: any) {
218
+ return `Failed to control Neovim diff view: ${e.message ?? e}. ` +
219
+ `Is Neovim running with --listen ${socket} and the diff-review plugin installed?`
220
+ }
221
+ },
222
+ }),
223
+ },
224
+ }
225
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "opencode-nvim-diff-review",
3
+ "version": "0.1.0",
4
+ "description": "Agent-driven guided code review for Neovim + OpenCode",
5
+ "main": "opencode-plugin/index.ts",
6
+ "keywords": [
7
+ "opencode",
8
+ "opencode-plugin",
9
+ "neovim",
10
+ "nvim",
11
+ "diff",
12
+ "diffview",
13
+ "code-review"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/talldan/nvim-diff-review-opencode-plugin.git"
19
+ },
20
+ "peerDependencies": {
21
+ "@opencode-ai/plugin": "*"
22
+ }
23
+ }