-
-
Notifications
You must be signed in to change notification settings - Fork 67
feat: add session-wise diff support using codediff.nvim #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
P.S. there's a type checking error between |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds session-wise diff functionality to opencode.nvim using the codediff.nvim plugin. It enables users to view all changes made during an opencode session by comparing repository state snapshots from the beginning to the end of a session.
Changes:
- Added
session_diffcommand accessible via the select menu that lets users pick and diff sessions - Implemented snapshot extraction from opencode's storage to create before/after directory comparisons
- Added
/sessionAPI endpoint client to fetch available sessions from the opencode server
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| lua/opencode/diff.lua | New module implementing session diff functionality with snapshot retrieval and git tree export |
| lua/opencode/ui/select.lua | Added "session diff" option to the select menu when codediff.nvim is available |
| lua/opencode/cli/client.lua | Added get_sessions API function to retrieve session list from opencode server |
| lua/opencode/config.lua | Enabled diff section by default in select menu configuration |
| lua/opencode.lua | Exported session_diff function to public API |
| README.md | Added documentation for the new session diff feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lua/opencode/diff.lua
Outdated
| local cleanup_group = vim.api.nvim_create_augroup("OpencodeDiffCleanup", { clear = false }) | ||
| vim.api.nvim_create_autocmd("TabClosed", { | ||
| group = cleanup_group, | ||
| once = true, | ||
| callback = function() | ||
| vim.fn.delete(dir1, "rf") | ||
| vim.fn.delete(dir2, "rf") | ||
| end, | ||
| }) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The autocmd group is created with clear = false, but multiple calls to session_diff() will create multiple autocmds in the same group. Each autocmd will capture different dir1/dir2 values in closures, but all autocmds will trigger on any tab close. This could lead to errors when trying to delete already-deleted directories. Consider either using clear = true or creating a unique group name per invocation, or better yet, attach the cleanup to specific buffer/window events instead of TabClosed.
lua/opencode/diff.lua
Outdated
| -- Use CodeDiff command to compare directories | ||
| vim.cmd("CodeDiff " .. vim.fn.fnameescape(dir1) .. " " .. vim.fn.fnameescape(dir2)) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no validation that the CodeDiff command executed successfully. If codediff.nvim is not properly loaded or the command fails, temp directories won't be cleaned up since the autocmd relies on a tab being created. Consider using pcall around vim.cmd or checking if the command exists before executing it, and ensure cleanup happens even on failure.
| -- Use CodeDiff command to compare directories | |
| vim.cmd("CodeDiff " .. vim.fn.fnameescape(dir1) .. " " .. vim.fn.fnameescape(dir2)) | |
| -- Ensure CodeDiff command is available | |
| if vim.fn.exists(":CodeDiff") == 0 then | |
| vim.notify("CodeDiff command not found. Is codediff.nvim installed and loaded?", vim.log.levels.ERROR, { | |
| title = "opencode", | |
| }) | |
| vim.fn.delete(dir1, "rf") | |
| vim.fn.delete(dir2, "rf") | |
| return | |
| end | |
| -- Use CodeDiff command to compare directories | |
| local ok, cmd_err = pcall(vim.cmd, "CodeDiff " .. vim.fn.fnameescape(dir1) .. " " .. vim.fn.fnameescape(dir2)) | |
| if not ok then | |
| vim.notify("Failed to run CodeDiff: " .. tostring(cmd_err), vim.log.levels.ERROR, { title = "opencode" }) | |
| vim.fn.delete(dir1, "rf") | |
| vim.fn.delete(dir2, "rf") | |
| return | |
| end |
| local function export_tree(snapshot_git, tree_hash, callback) | ||
| -- Create temp directory | ||
| local temp_dir = vim.fn.tempname() | ||
| vim.fn.mkdir(temp_dir, "p") | ||
|
|
||
| -- Verify the object exists | ||
| vim.system({ "git", "--git-dir=" .. snapshot_git, "cat-file", "-t", tree_hash }, { text = true }, function(result) | ||
| if result.code ~= 0 then | ||
| vim.schedule(function() | ||
| callback("Snapshot object no longer exists (may have been garbage collected): " .. tree_hash, nil) | ||
| end) | ||
| return | ||
| end | ||
|
|
||
| -- Export the tree using git archive | ||
| vim.system({ "git", "--git-dir=" .. snapshot_git, "archive", tree_hash }, { text = false }, function(archive_result) | ||
| if archive_result.code ~= 0 then | ||
| vim.schedule(function() | ||
| callback("Failed to archive tree: " .. (archive_result.stderr or ""), nil) | ||
| end) | ||
| return | ||
| end | ||
|
|
||
| -- Extract the archive | ||
| vim.system( | ||
| { "tar", "-xf", "-", "-C", temp_dir }, | ||
| { stdin = archive_result.stdout, text = false }, | ||
| function(tar_result) | ||
| vim.schedule(function() | ||
| if tar_result.code ~= 0 then | ||
| callback("Failed to extract archive: " .. (tar_result.stderr or ""), nil) | ||
| else | ||
| callback(nil, temp_dir) | ||
| end | ||
| end) | ||
| end | ||
| ) | ||
| end) | ||
| end) | ||
| end |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation uses Unix-specific commands (git, tar) and assumes Unix-style paths ($HOME/.local/share). This feature will likely not work on Windows systems. Consider adding a platform check and providing appropriate error messaging for Windows users, or implement Windows-compatible alternatives.
| vim.system({ "git", "--git-dir=" .. snapshot_git, "cat-file", "-t", tree_hash }, { text = true }, function(result) | ||
| if result.code ~= 0 then | ||
| vim.schedule(function() | ||
| callback("Snapshot object no longer exists (may have been garbage collected): " .. tree_hash, nil) | ||
| end) | ||
| return | ||
| end | ||
|
|
||
| -- Export the tree using git archive | ||
| vim.system({ "git", "--git-dir=" .. snapshot_git, "archive", tree_hash }, { text = false }, function(archive_result) | ||
| if archive_result.code ~= 0 then | ||
| vim.schedule(function() | ||
| callback("Failed to archive tree: " .. (archive_result.stderr or ""), nil) | ||
| end) | ||
| return | ||
| end |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the git cat-file or git archive commands fail after creating the temp directory, that directory is not cleaned up. The temp_dir created at line 133-134 should be deleted in the error paths at lines 140 and 149 to prevent temporary directory leaks.
lua/opencode/diff.lua
Outdated
| local cleanup_group = vim.api.nvim_create_augroup("OpencodeDiffCleanup", { clear = false }) | ||
| vim.api.nvim_create_autocmd("TabClosed", { | ||
| group = cleanup_group, | ||
| once = true, | ||
| callback = function() | ||
| vim.fn.delete(dir1, "rf") | ||
| vim.fn.delete(dir2, "rf") | ||
| end, | ||
| }) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TabClosed autocmd may not reliably clean up temporary directories. If the user closes the diff buffers or windows without closing the tab (e.g., using :bdelete or :close), the temporary directories will not be cleaned up and will persist on disk. Consider using BufDelete or WinClosed events on the specific buffers/windows created by CodeDiff, or track the tab number with vim.api.nvim_get_current_tabpage() and match it in the autocmd pattern.
lua/opencode/diff.lua
Outdated
| local STORAGE = vim.fn.expand("$HOME/.local/share/opencode/storage") | ||
| local SNAPSHOT_DIR = vim.fn.expand("$HOME/.local/share/opencode/snapshot") | ||
|
|
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded storage paths assume the opencode server uses the default XDG data directory. If the opencode server is configured with a custom storage location (e.g., via environment variables or configuration), this implementation will fail to find the snapshots. Consider making these paths configurable or detecting them from the opencode server's configuration.
| local STORAGE = vim.fn.expand("$HOME/.local/share/opencode/storage") | |
| local SNAPSHOT_DIR = vim.fn.expand("$HOME/.local/share/opencode/snapshot") | |
| ---Resolve the base data directory used by the opencode server. | |
| ---Order of precedence: | |
| --- 1. OPENCODE_DATA_DIR environment variable | |
| --- 2. vim.g.opencode_data_dir global (if set by config) | |
| --- 3. $XDG_DATA_HOME/opencode (if XDG_DATA_HOME is set) | |
| --- 4. $HOME/.local/share/opencode (current default) | |
| ---@return string | |
| local function get_opencode_data_dir() | |
| -- Explicit override via environment variable | |
| local env_dir = vim.env.OPENCODE_DATA_DIR | |
| if env_dir and env_dir ~= "" then | |
| return vim.fn.expand(env_dir) | |
| end | |
| -- Optional override via Neovim global (can be set in user config) | |
| local global_dir = vim.g.opencode_data_dir | |
| if type(global_dir) == "string" and global_dir ~= "" then | |
| return vim.fn.expand(global_dir) | |
| end | |
| -- Respect XDG data home if available | |
| local xdg_data_home = vim.env.XDG_DATA_HOME | |
| if xdg_data_home and xdg_data_home ~= "" then | |
| return xdg_data_home .. "/opencode" | |
| end | |
| -- Fallback to the original hardcoded default | |
| return vim.fn.expand("$HOME/.local/share/opencode") | |
| end | |
| local OPENCODE_DATA_DIR = get_opencode_data_dir() | |
| local STORAGE = OPENCODE_DATA_DIR .. "/storage" | |
| local SNAPSHOT_DIR = OPENCODE_DATA_DIR .. "/snapshot" |
|
A few changes with the last 3 commits:
|
Using codediff.nvim, this PR enables users to pick a session to view a diff of. It compares the state of the repository at the start of the session to that after the session by somewhat reverse-engineering using opencode's snapshots feature. Since that feature only works on git repositories, this diff feature also only works on git repositories.
Changes
session difftorequire("opencode").selectmenu, which can be toggled via select optsrequire("opencode").session_diffwithout codediff.nvim installedvim.ui.selectto let user pick which session to diff, fetched from/sessionendpoint of opencode serverAddresses #101 and #91.
Screenshots