Skip to content

Conversation

@naowalrahman
Copy link

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

  • adds session diff to require("opencode").select menu, which can be toggled via select opts
  • warns users if trying to run require("opencode").session_diff without codediff.nvim installed
  • uses vim.ui.select to let user pick which session to diff, fetched from /session endpoint of opencode server
  • creates before/after temp directories for the file states from the snapshots and diffs them with codediff.nvim

Addresses #101 and #91.

Screenshots

screenshot_2026-01-18_09-26-24-PM screenshot_2026-01-18_09-28-09-PM

Copilot AI review requested due to automatic review settings January 19, 2026 02:30
@naowalrahman
Copy link
Author

P.S. there's a type checking error between string|nil and string in diff.lua that I tried to mess around with but couldn't quite fix, so I'd appreciate any help fixing that 😅

Copy link

Copilot AI left a 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_diff command 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 /session API 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.

Comment on lines 224 to 232
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,
})
Copy link

Copilot AI Jan 19, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 220 to 221
-- Use CodeDiff command to compare directories
vim.cmd("CodeDiff " .. vim.fn.fnameescape(dir1) .. " " .. vim.fn.fnameescape(dir2))
Copy link

Copilot AI Jan 19, 2026

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.

Suggested change
-- 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

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +170
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
Copy link

Copilot AI Jan 19, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +152
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
Copy link

Copilot AI Jan 19, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 224 to 232
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,
})
Copy link

Copilot AI Jan 19, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 7
local STORAGE = vim.fn.expand("$HOME/.local/share/opencode/storage")
local SNAPSHOT_DIR = vim.fn.expand("$HOME/.local/share/opencode/snapshot")

Copy link

Copilot AI Jan 19, 2026

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.

Suggested change
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"

Copilot uses AI. Check for mistakes.
@naowalrahman naowalrahman mentioned this pull request Jan 19, 2026
12 tasks
@naowalrahman
Copy link
Author

A few changes with the last 3 commits:

  • fixed the LSP error so all checks pass now
  • removed temp directory cleanup all together because they get erased when you close neovim anyway
  • allow users to set their opencode data directory in opts, eliminating cross-platform storage directory differences

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant