claude-baseline-1752171490

This commit is contained in:
zolinthecow 2025-07-10 11:18:10 -07:00
parent ac6f7354b6
commit 91c24b34a6
5 changed files with 580 additions and 463 deletions

View File

@ -98,3 +98,6 @@ SEPARATED HUNK #4: Final test with better hunk separation!
MULTI-EDIT TEST #4: Testing hunk navigation with ]h and [h! MULTI-EDIT TEST #4: Testing hunk navigation with ]h and [h!
## PERSISTENCE TEST
This tests our new stash-based persistence system that doesn't create git commits!

View File

@ -7,7 +7,7 @@ M.current_review = M.current_review or nil
function M.setup() function M.setup()
-- Set up keybindings -- Set up keybindings
M.setup_keybindings() M.setup_keybindings()
vim.notify('Diff review system loaded (using diffview.nvim)', vim.log.levels.DEBUG) vim.notify('Diff review system loaded (using diffview.nvim)', vim.log.levels.DEBUG)
end end
@ -17,33 +17,29 @@ function M.handle_claude_edit(stash_ref, pre_edit_ref)
vim.notify('No stash reference provided for diff review', vim.log.levels.ERROR) vim.notify('No stash reference provided for diff review', vim.log.levels.ERROR)
return return
end end
vim.notify('Processing Claude edit with stash: ' .. stash_ref, vim.log.levels.INFO) vim.notify('Processing Claude edit with stash: ' .. stash_ref, vim.log.levels.INFO)
-- Get list of changed files -- Get list of changed files
local changed_files = M.get_changed_files(stash_ref) local changed_files = M.get_changed_files(stash_ref)
if not changed_files or #changed_files == 0 then if not changed_files or #changed_files == 0 then
vim.notify('No changes detected from Claude edit', vim.log.levels.INFO) vim.notify('No changes detected from Claude edit', vim.log.levels.INFO)
return return
end end
-- Initialize review session -- Initialize review session
M.current_review = { M.current_review = {
stash_ref = stash_ref, stash_ref = stash_ref,
pre_edit_ref = pre_edit_ref, -- Store the pre-edit commit reference pre_edit_ref = pre_edit_ref, -- Store the pre-edit commit reference
timestamp = os.time(), timestamp = os.time(),
changed_files = changed_files, changed_files = changed_files,
} }
-- Notify user about changes -- Notify user about changes
vim.notify(string.format( vim.notify(string.format('Claude made changes to %d file(s): %s', #changed_files, table.concat(changed_files, ', ')), vim.log.levels.INFO)
'Claude made changes to %d file(s): %s',
#changed_files,
table.concat(changed_files, ', ')
), vim.log.levels.INFO)
vim.notify('Use <leader>dd to open diffview, <leader>df for fugitive, <leader>dc to clear review', vim.log.levels.INFO) vim.notify('Use <leader>dd to open diffview, <leader>df for fugitive, <leader>dc to clear review', vim.log.levels.INFO)
-- Automatically open diffview -- Automatically open diffview
M.open_diffview() M.open_diffview()
end end
@ -54,31 +50,28 @@ function M.handle_claude_stashes(baseline_ref)
vim.notify('No baseline reference provided for Claude stashes', vim.log.levels.ERROR) vim.notify('No baseline reference provided for Claude stashes', vim.log.levels.ERROR)
return return
end end
vim.notify('Showing Claude stashes against baseline: ' .. baseline_ref, vim.log.levels.INFO) vim.notify('Showing Claude stashes against baseline: ' .. baseline_ref, vim.log.levels.INFO)
-- Get Claude stashes -- Get Claude stashes
local claude_stashes = M.get_claude_stashes() local claude_stashes = M.get_claude_stashes()
if not claude_stashes or #claude_stashes == 0 then if not claude_stashes or #claude_stashes == 0 then
vim.notify('No Claude stashes found', vim.log.levels.INFO) vim.notify('No Claude stashes found', vim.log.levels.INFO)
return return
end end
-- Initialize review session for Claude stashes -- Initialize review session for Claude stashes
M.current_review = { M.current_review = {
baseline_ref = baseline_ref, baseline_ref = baseline_ref,
timestamp = os.time(), timestamp = os.time(),
claude_stashes = claude_stashes, claude_stashes = claude_stashes,
current_stash_index = 0, -- Show cumulative view by default current_stash_index = 0, -- Show cumulative view by default
is_stash_based = true is_stash_based = true,
} }
-- Notify user about changes -- Notify user about changes
vim.notify(string.format( vim.notify(string.format('Found %d Claude stash(es). Use <leader>dd for cumulative view, <leader>dh to browse.', #claude_stashes), vim.log.levels.INFO)
'Found %d Claude stash(es). Use <leader>dd for cumulative view, <leader>dh to browse.',
#claude_stashes
), vim.log.levels.INFO)
-- Automatically open cumulative stash view -- Automatically open cumulative stash view
M.open_cumulative_stash_view() M.open_cumulative_stash_view()
end end
@ -91,16 +84,16 @@ end
-- Get list of files changed in the stash -- Get list of files changed in the stash
function M.get_changed_files(stash_ref) function M.get_changed_files(stash_ref)
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local cmd = string.format('git stash show %s --name-only', stash_ref) local cmd = string.format('git stash show %s --name-only', stash_ref)
local result = utils.exec(cmd) local result = utils.exec(cmd)
if not result or result == '' then if not result or result == '' then
return {} return {}
end end
local files = {} local files = {}
for line in result:gmatch('[^\n]+') do for line in result:gmatch '[^\n]+' do
if line ~= '' then if line ~= '' then
table.insert(files, line) table.insert(files, line)
end end
@ -110,16 +103,16 @@ end
-- Get list of files changed since baseline -- Get list of files changed since baseline
function M.get_changed_files_since_baseline(baseline_ref) function M.get_changed_files_since_baseline(baseline_ref)
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local cmd = string.format('git diff --name-only %s', baseline_ref) local cmd = string.format('git diff --name-only %s', baseline_ref)
local result = utils.exec(cmd) local result = utils.exec(cmd)
if not result or result == '' then if not result or result == '' then
return {} return {}
end end
local files = {} local files = {}
for line in result:gmatch('[^\n]+') do for line in result:gmatch '[^\n]+' do
if line ~= '' then if line ~= '' then
table.insert(files, line) table.insert(files, line)
end end
@ -129,22 +122,22 @@ end
-- Get Claude stashes (only stashes with [claude-edit] messages) -- Get Claude stashes (only stashes with [claude-edit] messages)
function M.get_claude_stashes() function M.get_claude_stashes()
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local cmd = 'git stash list' local cmd = 'git stash list'
local result = utils.exec(cmd) local result = utils.exec(cmd)
if not result or result == '' then if not result or result == '' then
return {} return {}
end end
local stashes = {} local stashes = {}
for line in result:gmatch('[^\n]+') do for line in result:gmatch '[^\n]+' do
if line ~= '' and line:match('%[claude%-edit%]') then if line ~= '' and line:match '%[claude%-edit%]' then
local stash_ref = line:match('^(stash@{%d+})') local stash_ref = line:match '^(stash@{%d+})'
if stash_ref then if stash_ref then
table.insert(stashes, { table.insert(stashes, {
ref = stash_ref, ref = stash_ref,
message = line:match(': (.+)$') or line message = line:match ': (.+)$' or line,
}) })
end end
end end
@ -161,15 +154,15 @@ function M.setup_keybindings()
vim.keymap.set('n', '<leader>dl', M.list_changes, { desc = 'List Claude changed files' }) vim.keymap.set('n', '<leader>dl', M.list_changes, { desc = 'List Claude changed files' })
vim.keymap.set('n', '<leader>da', M.accept_changes, { desc = 'Accept all Claude changes' }) vim.keymap.set('n', '<leader>da', M.accept_changes, { desc = 'Accept all Claude changes' })
vim.keymap.set('n', '<leader>dr', M.decline_changes, { desc = 'Decline all Claude changes' }) vim.keymap.set('n', '<leader>dr', M.decline_changes, { desc = 'Decline all Claude changes' })
-- Stash browsing -- Stash browsing
vim.keymap.set('n', '<leader>dh', M.browse_claude_stashes, { desc = 'Browse Claude stash history' }) vim.keymap.set('n', '<leader>dh', M.browse_claude_stashes, { desc = 'Browse Claude stash history' })
vim.keymap.set('n', '<leader>dp', M.previous_stash, { desc = 'View previous Claude stash' }) vim.keymap.set('n', '<leader>dp', M.previous_stash, { desc = 'View previous Claude stash' })
vim.keymap.set('n', '<leader>dn', M.next_stash, { desc = 'View next Claude stash' }) vim.keymap.set('n', '<leader>dn', M.next_stash, { desc = 'View next Claude stash' })
-- Unified view -- Unified view
vim.keymap.set('n', '<leader>du', M.open_unified_view, { desc = 'Open Claude diff in unified view' }) vim.keymap.set('n', '<leader>du', M.open_unified_view, { desc = 'Open Claude diff in unified view' })
-- Hunk operations -- Hunk operations
vim.keymap.set('n', '<leader>dka', M.accept_hunk_at_cursor, { desc = 'Accept Claude hunk at cursor' }) vim.keymap.set('n', '<leader>dka', M.accept_hunk_at_cursor, { desc = 'Accept Claude hunk at cursor' })
vim.keymap.set('n', '<leader>dkr', M.reject_hunk_at_cursor, { desc = 'Reject Claude hunk at cursor' }) vim.keymap.set('n', '<leader>dkr', M.reject_hunk_at_cursor, { desc = 'Reject Claude hunk at cursor' })
@ -179,9 +172,9 @@ end
function M.open_diffview() function M.open_diffview()
if not M.current_review then if not M.current_review then
-- Try to recover stash-based session from baseline -- Try to recover stash-based session from baseline
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local baseline_ref = utils.read_file('/tmp/claude-baseline-commit') local baseline_ref = utils.read_file '/tmp/claude-baseline-commit'
-- If no baseline file, but we have Claude stashes, use HEAD as baseline -- If no baseline file, but we have Claude stashes, use HEAD as baseline
local claude_stashes = M.get_claude_stashes() local claude_stashes = M.get_claude_stashes()
if claude_stashes and #claude_stashes > 0 then if claude_stashes and #claude_stashes > 0 then
@ -191,35 +184,35 @@ function M.open_diffview()
else else
baseline_ref = baseline_ref:gsub('%s+', '') baseline_ref = baseline_ref:gsub('%s+', '')
end end
M.current_review = { M.current_review = {
baseline_ref = baseline_ref, baseline_ref = baseline_ref,
timestamp = os.time(), timestamp = os.time(),
claude_stashes = claude_stashes, claude_stashes = claude_stashes,
current_stash_index = 0, -- Show cumulative view by default current_stash_index = 0, -- Show cumulative view by default
is_stash_based = true is_stash_based = true,
} }
vim.notify(string.format('Recovered Claude stash session with %d stashes', #claude_stashes), vim.log.levels.INFO) vim.notify(string.format('Recovered Claude stash session with %d stashes', #claude_stashes), vim.log.levels.INFO)
end end
if not M.current_review then if not M.current_review then
vim.notify('No active review session', vim.log.levels.INFO) vim.notify('No active review session', vim.log.levels.INFO)
return return
end end
end end
-- Use stash-based diff if available -- Use stash-based diff if available
if M.current_review.is_stash_based then if M.current_review.is_stash_based then
M.open_cumulative_stash_view() M.open_cumulative_stash_view()
return return
end end
-- Legacy: Use cumulative diff if available -- Legacy: Use cumulative diff if available
if M.current_review.is_cumulative then if M.current_review.is_cumulative then
M.open_cumulative_diffview() M.open_cumulative_diffview()
return return
end end
-- Check if diffview is available -- Check if diffview is available
local ok, diffview = pcall(require, 'diffview') local ok, diffview = pcall(require, 'diffview')
if not ok then if not ok then
@ -227,7 +220,7 @@ function M.open_diffview()
M.open_fugitive() M.open_fugitive()
return return
end end
-- Use the pre-edit reference if available -- Use the pre-edit reference if available
if M.current_review.pre_edit_ref then if M.current_review.pre_edit_ref then
local cmd = 'DiffviewOpen ' .. M.current_review.pre_edit_ref local cmd = 'DiffviewOpen ' .. M.current_review.pre_edit_ref
@ -248,14 +241,14 @@ function M.open_cumulative_stash_view()
vim.notify('No active review session', vim.log.levels.INFO) vim.notify('No active review session', vim.log.levels.INFO)
return return
end end
-- Check if diffview is available -- Check if diffview is available
local ok, diffview = pcall(require, 'diffview') local ok, diffview = pcall(require, 'diffview')
if not ok then if not ok then
vim.notify('diffview.nvim not available', vim.log.levels.WARN) vim.notify('diffview.nvim not available', vim.log.levels.WARN)
return return
end end
if M.current_review.is_stash_based and M.current_review.claude_stashes then if M.current_review.is_stash_based and M.current_review.claude_stashes then
-- Show cumulative diff of all Claude stashes against baseline -- Show cumulative diff of all Claude stashes against baseline
local cmd = 'DiffviewOpen ' .. M.current_review.baseline_ref local cmd = 'DiffviewOpen ' .. M.current_review.baseline_ref
@ -280,7 +273,7 @@ function M.open_fugitive()
vim.notify('No active review session', vim.log.levels.INFO) vim.notify('No active review session', vim.log.levels.INFO)
return return
end end
-- Use fugitive to show diff -- Use fugitive to show diff
local cmd = 'Gdiffsplit ' .. M.current_review.stash_ref local cmd = 'Gdiffsplit ' .. M.current_review.stash_ref
vim.notify('Opening fugitive: ' .. cmd, vim.log.levels.INFO) vim.notify('Opening fugitive: ' .. cmd, vim.log.levels.INFO)
@ -293,13 +286,13 @@ function M.list_changes()
vim.notify('No active review session', vim.log.levels.INFO) vim.notify('No active review session', vim.log.levels.INFO)
return return
end end
local files = M.current_review.changed_files local files = M.current_review.changed_files
if #files == 0 then if #files == 0 then
vim.notify('No changes found', vim.log.levels.INFO) vim.notify('No changes found', vim.log.levels.INFO)
return return
end end
-- Create a telescope picker if available, otherwise just notify -- Create a telescope picker if available, otherwise just notify
local ok, telescope = pcall(require, 'telescope.pickers') local ok, telescope = pcall(require, 'telescope.pickers')
if ok then if ok then
@ -314,38 +307,40 @@ end
-- Telescope picker for changed files -- Telescope picker for changed files
function M.telescope_changed_files() function M.telescope_changed_files()
local pickers = require('telescope.pickers') local pickers = require 'telescope.pickers'
local finders = require('telescope.finders') local finders = require 'telescope.finders'
local conf = require('telescope.config').values local conf = require('telescope.config').values
pickers.new({}, { pickers
prompt_title = 'Claude Changed Files', .new({}, {
finder = finders.new_table({ prompt_title = 'Claude Changed Files',
results = M.current_review.changed_files, finder = finders.new_table {
}), results = M.current_review.changed_files,
sorter = conf.generic_sorter({}), },
attach_mappings = function(_, map) sorter = conf.generic_sorter {},
map('i', '<CR>', function(prompt_bufnr) attach_mappings = function(_, map)
local selection = require('telescope.actions.state').get_selected_entry() map('i', '<CR>', function(prompt_bufnr)
require('telescope.actions').close(prompt_bufnr) local selection = require('telescope.actions.state').get_selected_entry()
vim.cmd('edit ' .. selection[1]) require('telescope.actions').close(prompt_bufnr)
M.open_diffview() vim.cmd('edit ' .. selection[1])
end) M.open_diffview()
return true end)
end, return true
}):find() end,
})
:find()
end end
-- Clear review session -- Clear review session
function M.clear_review() function M.clear_review()
if M.current_review then if M.current_review then
M.current_review = nil M.current_review = nil
-- Close diffview if it's open -- Close diffview if it's open
pcall(function() pcall(function()
vim.cmd('DiffviewClose') vim.cmd 'DiffviewClose'
end) end)
vim.notify('Claude review session cleared', vim.log.levels.INFO) vim.notify('Claude review session cleared', vim.log.levels.INFO)
else else
vim.notify('No active Claude review session', vim.log.levels.INFO) vim.notify('No active Claude review session', vim.log.levels.INFO)
@ -354,94 +349,94 @@ end
-- Accept all Claude changes (update baseline) -- Accept all Claude changes (update baseline)
function M.accept_changes() function M.accept_changes()
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
-- Get project root -- Get project root
local git_root = utils.get_project_root() local git_root = utils.get_project_root()
if not git_root then if not git_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR) vim.notify('Not in a git repository', vim.log.levels.ERROR)
return return
end end
-- Create new baseline commit with current state -- Create new baseline commit with current state
local timestamp = os.time() local timestamp = os.time()
local commit_msg = string.format('claude-baseline-%d', timestamp) local commit_msg = string.format('claude-baseline-%d', timestamp)
-- Stage all changes -- Stage all changes
local add_cmd = string.format('cd "%s" && git add -A', git_root) local add_cmd = string.format('cd "%s" && git add -A', git_root)
local add_result, add_err = utils.exec(add_cmd) local add_result, add_err = utils.exec(add_cmd)
if add_err then if add_err then
vim.notify('Failed to stage changes: ' .. add_err, vim.log.levels.ERROR) vim.notify('Failed to stage changes: ' .. add_err, vim.log.levels.ERROR)
return return
end end
-- Create new baseline commit -- Create new baseline commit
local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg) local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg)
local commit_result, commit_err = utils.exec(commit_cmd) local commit_result, commit_err = utils.exec(commit_cmd)
if commit_err and not commit_err:match('nothing to commit') then if commit_err and not commit_err:match 'nothing to commit' then
vim.notify('Failed to create new baseline: ' .. commit_err, vim.log.levels.ERROR) vim.notify('Failed to create new baseline: ' .. commit_err, vim.log.levels.ERROR)
return return
end end
-- Update baseline reference -- Update baseline reference
local baseline_file = '/tmp/claude-baseline-commit' local baseline_file = '/tmp/claude-baseline-commit'
utils.write_file(baseline_file, 'HEAD') utils.write_file(baseline_file, 'HEAD')
-- Clear review session -- Clear review session
M.current_review = nil M.current_review = nil
-- Close diffview -- Close diffview
pcall(function() pcall(function()
vim.cmd('DiffviewClose') vim.cmd 'DiffviewClose'
end) end)
vim.notify('All Claude changes accepted! New baseline created.', vim.log.levels.INFO) vim.notify('All Claude changes accepted! New baseline created.', vim.log.levels.INFO)
end end
-- Decline all Claude changes (reset to baseline) -- Decline all Claude changes (reset to baseline)
function M.decline_changes() function M.decline_changes()
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
-- Get baseline commit -- Get baseline commit
local baseline_file = '/tmp/claude-baseline-commit' local baseline_file = '/tmp/claude-baseline-commit'
local baseline_ref = utils.read_file(baseline_file) local baseline_ref = utils.read_file(baseline_file)
if not baseline_ref or baseline_ref == '' then if not baseline_ref or baseline_ref == '' then
vim.notify('No baseline commit found', vim.log.levels.ERROR) vim.notify('No baseline commit found', vim.log.levels.ERROR)
return return
end end
baseline_ref = baseline_ref:gsub('%s+', '') baseline_ref = baseline_ref:gsub('%s+', '')
-- Get project root -- Get project root
local git_root = utils.get_project_root() local git_root = utils.get_project_root()
if not git_root then if not git_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR) vim.notify('Not in a git repository', vim.log.levels.ERROR)
return return
end end
-- Reset to baseline (hard reset) -- Reset to baseline (hard reset)
local reset_cmd = string.format('cd "%s" && git reset --hard %s', git_root, baseline_ref) local reset_cmd = string.format('cd "%s" && git reset --hard %s', git_root, baseline_ref)
local reset_result, reset_err = utils.exec(reset_cmd) local reset_result, reset_err = utils.exec(reset_cmd)
if reset_err then if reset_err then
vim.notify('Failed to reset to baseline: ' .. reset_err, vim.log.levels.ERROR) vim.notify('Failed to reset to baseline: ' .. reset_err, vim.log.levels.ERROR)
return return
end end
-- Clear review session -- Clear review session
M.current_review = nil M.current_review = nil
-- Close diffview -- Close diffview
pcall(function() pcall(function()
vim.cmd('DiffviewClose') vim.cmd 'DiffviewClose'
end) end)
-- Refresh buffers -- Refresh buffers
vim.cmd('checktime') vim.cmd 'checktime'
vim.notify('All Claude changes declined! Reset to baseline.', vim.log.levels.INFO) vim.notify('All Claude changes declined! Reset to baseline.', vim.log.levels.INFO)
end end
@ -451,13 +446,13 @@ function M.browse_claude_stashes()
vim.notify('No Claude stash session active', vim.log.levels.INFO) vim.notify('No Claude stash session active', vim.log.levels.INFO)
return return
end end
local stashes = M.current_review.claude_stashes local stashes = M.current_review.claude_stashes
if not stashes or #stashes == 0 then if not stashes or #stashes == 0 then
vim.notify('No Claude stashes found', vim.log.levels.INFO) vim.notify('No Claude stashes found', vim.log.levels.INFO)
return return
end end
-- Create a telescope picker if available, otherwise just notify -- Create a telescope picker if available, otherwise just notify
local ok, telescope = pcall(require, 'telescope.pickers') local ok, telescope = pcall(require, 'telescope.pickers')
if ok then if ok then
@ -477,19 +472,19 @@ function M.previous_stash()
vim.notify('No Claude stash session active', vim.log.levels.INFO) vim.notify('No Claude stash session active', vim.log.levels.INFO)
return return
end end
local stashes = M.current_review.claude_stashes local stashes = M.current_review.claude_stashes
if not stashes or #stashes == 0 then if not stashes or #stashes == 0 then
vim.notify('No Claude stashes found', vim.log.levels.INFO) vim.notify('No Claude stashes found', vim.log.levels.INFO)
return return
end end
local current_index = M.current_review.current_stash_index or 0 local current_index = M.current_review.current_stash_index or 0
if current_index <= 1 then if current_index <= 1 then
vim.notify('Already at first stash', vim.log.levels.INFO) vim.notify('Already at first stash', vim.log.levels.INFO)
return return
end end
M.current_review.current_stash_index = current_index - 1 M.current_review.current_stash_index = current_index - 1
M.view_specific_stash(M.current_review.current_stash_index) M.view_specific_stash(M.current_review.current_stash_index)
end end
@ -500,19 +495,19 @@ function M.next_stash()
vim.notify('No Claude stash session active', vim.log.levels.INFO) vim.notify('No Claude stash session active', vim.log.levels.INFO)
return return
end end
local stashes = M.current_review.claude_stashes local stashes = M.current_review.claude_stashes
if not stashes or #stashes == 0 then if not stashes or #stashes == 0 then
vim.notify('No Claude stashes found', vim.log.levels.INFO) vim.notify('No Claude stashes found', vim.log.levels.INFO)
return return
end end
local current_index = M.current_review.current_stash_index or 0 local current_index = M.current_review.current_stash_index or 0
if current_index >= #stashes then if current_index >= #stashes then
vim.notify('Already at last stash', vim.log.levels.INFO) vim.notify('Already at last stash', vim.log.levels.INFO)
return return
end end
M.current_review.current_stash_index = current_index + 1 M.current_review.current_stash_index = current_index + 1
M.view_specific_stash(M.current_review.current_stash_index) M.view_specific_stash(M.current_review.current_stash_index)
end end
@ -523,22 +518,22 @@ function M.view_specific_stash(index)
vim.notify('No Claude stash session active', vim.log.levels.INFO) vim.notify('No Claude stash session active', vim.log.levels.INFO)
return return
end end
local stashes = M.current_review.claude_stashes local stashes = M.current_review.claude_stashes
if not stashes or index < 1 or index > #stashes then if not stashes or index < 1 or index > #stashes then
vim.notify('Invalid stash index', vim.log.levels.ERROR) vim.notify('Invalid stash index', vim.log.levels.ERROR)
return return
end end
local stash = stashes[index] local stash = stashes[index]
-- Check if diffview is available -- Check if diffview is available
local ok, diffview = pcall(require, 'diffview') local ok, diffview = pcall(require, 'diffview')
if not ok then if not ok then
vim.notify('diffview.nvim not available', vim.log.levels.WARN) vim.notify('diffview.nvim not available', vim.log.levels.WARN)
return return
end end
-- Open diffview for this specific stash -- Open diffview for this specific stash
local cmd = string.format('DiffviewOpen %s^..%s', stash.ref, stash.ref) local cmd = string.format('DiffviewOpen %s^..%s', stash.ref, stash.ref)
vim.notify(string.format('Opening stash %d: %s', index, stash.message), vim.log.levels.INFO) vim.notify(string.format('Opening stash %d: %s', index, stash.message), vim.log.levels.INFO)
@ -547,13 +542,13 @@ end
-- Telescope picker for Claude stashes -- Telescope picker for Claude stashes
function M.telescope_claude_stashes() function M.telescope_claude_stashes()
local pickers = require('telescope.pickers') local pickers = require 'telescope.pickers'
local finders = require('telescope.finders') local finders = require 'telescope.finders'
local conf = require('telescope.config').values local conf = require('telescope.config').values
local stashes = M.current_review.claude_stashes local stashes = M.current_review.claude_stashes
local stash_entries = {} local stash_entries = {}
for i, stash in ipairs(stashes) do for i, stash in ipairs(stashes) do
table.insert(stash_entries, { table.insert(stash_entries, {
value = i, value = i,
@ -561,30 +556,32 @@ function M.telescope_claude_stashes()
ordinal = stash.message, ordinal = stash.message,
}) })
end end
pickers.new({}, { pickers
prompt_title = 'Claude Stash History', .new({}, {
finder = finders.new_table({ prompt_title = 'Claude Stash History',
results = stash_entries, finder = finders.new_table {
entry_maker = function(entry) results = stash_entries,
return { entry_maker = function(entry)
value = entry.value, return {
display = entry.display, value = entry.value,
ordinal = entry.ordinal, display = entry.display,
} ordinal = entry.ordinal,
}
end,
},
sorter = conf.generic_sorter {},
attach_mappings = function(_, map)
map('i', '<CR>', function(prompt_bufnr)
local selection = require('telescope.actions.state').get_selected_entry()
require('telescope.actions').close(prompt_bufnr)
M.current_review.current_stash_index = selection.value
M.view_specific_stash(selection.value)
end)
return true
end, end,
}), })
sorter = conf.generic_sorter({}), :find()
attach_mappings = function(_, map)
map('i', '<CR>', function(prompt_bufnr)
local selection = require('telescope.actions.state').get_selected_entry()
require('telescope.actions').close(prompt_bufnr)
M.current_review.current_stash_index = selection.value
M.view_specific_stash(selection.value)
end)
return true
end,
}):find()
end end
-- Generate combined patch from all Claude stashes -- Generate combined patch from all Claude stashes
@ -593,19 +590,19 @@ function M.generate_claude_patch()
vim.notify('No Claude stash session active', vim.log.levels.ERROR) vim.notify('No Claude stash session active', vim.log.levels.ERROR)
return nil return nil
end end
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local baseline_ref = M.current_review.baseline_ref local baseline_ref = M.current_review.baseline_ref
-- Generate diff from baseline to current working directory -- Generate diff from baseline to current working directory
local cmd = string.format('git diff %s', baseline_ref) local cmd = string.format('git diff %s', baseline_ref)
local patch, err = utils.exec(cmd) local patch, err = utils.exec(cmd)
if err then if err then
vim.notify('Failed to generate patch: ' .. err, vim.log.levels.ERROR) vim.notify('Failed to generate patch: ' .. err, vim.log.levels.ERROR)
return nil return nil
end end
return patch return patch
end end
@ -613,9 +610,9 @@ end
function M.open_unified_view() function M.open_unified_view()
if not M.current_review then if not M.current_review then
-- Try to recover stash-based session from baseline -- Try to recover stash-based session from baseline
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
local baseline_ref = utils.read_file('/tmp/claude-baseline-commit') local baseline_ref = utils.read_file '/tmp/claude-baseline-commit'
-- If no baseline file, but we have Claude stashes, use HEAD as baseline -- If no baseline file, but we have Claude stashes, use HEAD as baseline
local claude_stashes = M.get_claude_stashes() local claude_stashes = M.get_claude_stashes()
if claude_stashes and #claude_stashes > 0 then if claude_stashes and #claude_stashes > 0 then
@ -625,23 +622,23 @@ function M.open_unified_view()
else else
baseline_ref = baseline_ref:gsub('%s+', '') baseline_ref = baseline_ref:gsub('%s+', '')
end end
M.current_review = { M.current_review = {
baseline_ref = baseline_ref, baseline_ref = baseline_ref,
timestamp = os.time(), timestamp = os.time(),
claude_stashes = claude_stashes, claude_stashes = claude_stashes,
current_stash_index = 0, current_stash_index = 0,
is_stash_based = true is_stash_based = true,
} }
vim.notify(string.format('Recovered Claude stash session with %d stashes', #claude_stashes), vim.log.levels.INFO) vim.notify(string.format('Recovered Claude stash session with %d stashes', #claude_stashes), vim.log.levels.INFO)
end end
if not M.current_review then if not M.current_review then
vim.notify('No active review session', vim.log.levels.INFO) vim.notify('No active review session', vim.log.levels.INFO)
return return
end end
end end
-- Check if unified.nvim is available and load it -- Check if unified.nvim is available and load it
local ok, unified = pcall(require, 'unified') local ok, unified = pcall(require, 'unified')
if not ok then if not ok then
@ -649,56 +646,52 @@ function M.open_unified_view()
M.open_diffview() M.open_diffview()
return return
end end
-- Ensure unified.nvim is set up -- Ensure unified.nvim is set up
pcall(unified.setup, {}) pcall(unified.setup, {})
-- Use unified.nvim to show diff against baseline -- Use unified.nvim to show diff against baseline
local baseline_ref = M.current_review.baseline_ref local baseline_ref = M.current_review.baseline_ref
-- Try the command with pcall to catch errors -- Try the command with pcall to catch errors
local cmd_ok, cmd_err = pcall(function() local cmd_ok, cmd_err = pcall(function()
vim.cmd('Unified ' .. baseline_ref) vim.cmd('Unified ' .. baseline_ref)
end) end)
if not cmd_ok then if not cmd_ok then
vim.notify('Unified command failed: ' .. tostring(cmd_err) .. ', falling back to diffview', vim.log.levels.WARN) vim.notify('Unified command failed: ' .. tostring(cmd_err) .. ', falling back to diffview', vim.log.levels.WARN)
M.open_diffview() M.open_diffview()
return return
end end
vim.notify('Claude unified diff opened. Use ]h/[h to navigate hunks', vim.log.levels.INFO) vim.notify('Claude unified diff opened. Use ]h/[h to navigate hunks', vim.log.levels.INFO)
end end
-- Accept hunk at cursor position -- Accept hunk at cursor position
function M.accept_hunk_at_cursor() function M.accept_hunk_at_cursor()
-- Get current buffer and check if we're in a diff view -- Get current buffer and check if we're in a diff view
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local filetype = vim.bo.filetype local filetype = vim.bo.filetype
-- Check for various diff view types -- Check for various diff view types
local is_diff_view = bufname:match('diffview://') or local is_diff_view = bufname:match 'diffview://' or bufname:match 'Claude Unified Diff' or filetype == 'diff' or filetype == 'git'
bufname:match('Claude Unified Diff') or
filetype == 'diff' or
filetype == 'git'
if not is_diff_view then if not is_diff_view then
vim.notify('This command only works in diff views', vim.log.levels.WARN) vim.notify('This command only works in diff views', vim.log.levels.WARN)
return return
end end
-- Get current file and line from cursor position -- Get current file and line from cursor position
local cursor_line = vim.api.nvim_win_get_cursor(0)[1] local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
-- Parse diff to find current hunk -- Parse diff to find current hunk
local hunk_info = M.find_hunk_at_line(lines, cursor_line) local hunk_info = M.find_hunk_at_line(lines, cursor_line)
if not hunk_info then if not hunk_info then
vim.notify('No hunk found at cursor position', vim.log.levels.WARN) vim.notify('No hunk found at cursor position', vim.log.levels.WARN)
return return
end end
-- Apply the hunk -- Apply the hunk
M.apply_hunk(hunk_info) M.apply_hunk(hunk_info)
end end
@ -708,30 +701,30 @@ function M.reject_hunk_at_cursor()
-- Get current buffer and check if we're in a diff view -- Get current buffer and check if we're in a diff view
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local filetype = vim.bo.filetype local filetype = vim.bo.filetype
-- Check for various diff view types -- Check for various diff view types
local is_diff_view = bufname:match('diffview://') or local is_diff_view = bufname:match 'diffview://' or bufname:match 'Claude Unified Diff' or filetype == 'diff' or filetype == 'git'
bufname:match('Claude Unified Diff') or
filetype == 'diff' or
filetype == 'git'
if not is_diff_view then if not is_diff_view then
vim.notify('This command only works in diff views', vim.log.levels.WARN) vim.notify('This command only works in diff views', vim.log.levels.WARN)
return return
end end
-- Get current file and line from cursor position -- Get current file and line from cursor position
local cursor_line = vim.api.nvim_win_get_cursor(0)[1] local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
-- Parse diff to find current hunk -- Parse diff to find current hunk
local hunk_info = M.find_hunk_at_line(lines, cursor_line) local hunk_info = M.find_hunk_at_line(lines, cursor_line)
if not hunk_info then if not hunk_info then
vim.notify('No hunk found at cursor position', vim.log.levels.WARN) vim.notify('No hunk found at cursor position', vim.log.levels.WARN)
return return
end end
vim.notify(string.format('Rejected hunk in %s at lines %d-%d', hunk_info.file, hunk_info.old_start, hunk_info.old_start + hunk_info.old_count - 1), vim.log.levels.INFO) vim.notify(
string.format('Rejected hunk in %s at lines %d-%d', hunk_info.file, hunk_info.old_start, hunk_info.old_start + hunk_info.old_count - 1),
vim.log.levels.INFO
)
end end
-- Find hunk information at given line in diff buffer -- Find hunk information at given line in diff buffer
@ -740,29 +733,29 @@ function M.find_hunk_at_line(lines, target_line)
local in_hunk = false local in_hunk = false
local hunk_start_line = nil local hunk_start_line = nil
local hunk_lines = {} local hunk_lines = {}
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
-- File header -- File header
if line:match('^diff %-%-git') or line:match('^diff %-%-cc') then if line:match '^diff %-%-git' or line:match '^diff %-%-cc' then
current_file = line:match('b/(.+)$') current_file = line:match 'b/(.+)$'
elseif line:match('^%+%+%+ b/(.+)') then elseif line:match '^%+%+%+ b/(.+)' then
current_file = line:match('^%+%+%+ b/(.+)') current_file = line:match '^%+%+%+ b/(.+)'
end end
-- Hunk header -- Hunk header
if line:match('^@@') then if line:match '^@@' then
-- If we were in a hunk that included target line, return it -- If we were in a hunk that included target line, return it
if in_hunk and hunk_start_line and target_line >= hunk_start_line and target_line < i then if in_hunk and hunk_start_line and target_line >= hunk_start_line and target_line < i then
return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line) return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line)
end end
-- Start new hunk -- Start new hunk
in_hunk = true in_hunk = true
hunk_start_line = i hunk_start_line = i
hunk_lines = {line} hunk_lines = { line }
elseif in_hunk then elseif in_hunk then
-- Collect hunk lines -- Collect hunk lines
if line:match('^[%+%-%s]') then if line:match '^[%+%-%s]' then
table.insert(hunk_lines, line) table.insert(hunk_lines, line)
else else
-- End of hunk -- End of hunk
@ -773,24 +766,28 @@ function M.find_hunk_at_line(lines, target_line)
end end
end end
end end
-- Check last hunk -- Check last hunk
if in_hunk and hunk_start_line and target_line >= hunk_start_line then if in_hunk and hunk_start_line and target_line >= hunk_start_line then
return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line) return M.parse_hunk_info(hunk_lines, current_file, hunk_start_line)
end end
return nil return nil
end end
-- Parse hunk information from diff lines -- Parse hunk information from diff lines
function M.parse_hunk_info(hunk_lines, file, start_line) function M.parse_hunk_info(hunk_lines, file, start_line)
if #hunk_lines == 0 then return nil end if #hunk_lines == 0 then
return nil
end
local header = hunk_lines[1] local header = hunk_lines[1]
local old_start, old_count, new_start, new_count = header:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@') local old_start, old_count, new_start, new_count = header:match '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@'
if not old_start then return nil end if not old_start then
return nil
end
return { return {
file = file, file = file,
old_start = tonumber(old_start), old_start = tonumber(old_start),
@ -798,58 +795,59 @@ function M.parse_hunk_info(hunk_lines, file, start_line)
new_start = tonumber(new_start), new_start = tonumber(new_start),
new_count = tonumber(new_count) or 1, new_count = tonumber(new_count) or 1,
lines = hunk_lines, lines = hunk_lines,
buffer_start_line = start_line buffer_start_line = start_line,
} }
end end
-- Apply a specific hunk to the working directory -- Apply a specific hunk to the working directory
function M.apply_hunk(hunk_info) function M.apply_hunk(hunk_info)
local utils = require('nvim-claude.utils') local utils = require 'nvim-claude.utils'
-- Create a patch with just this hunk -- Create a patch with just this hunk
local patch_lines = { local patch_lines = {
'diff --git a/' .. hunk_info.file .. ' b/' .. hunk_info.file, 'diff --git a/' .. hunk_info.file .. ' b/' .. hunk_info.file,
'index 0000000..0000000 100644', 'index 0000000..0000000 100644',
'--- a/' .. hunk_info.file, '--- a/' .. hunk_info.file,
'+++ b/' .. hunk_info.file '+++ b/' .. hunk_info.file,
} }
-- Add hunk lines -- Add hunk lines
for _, line in ipairs(hunk_info.lines) do for _, line in ipairs(hunk_info.lines) do
table.insert(patch_lines, line) table.insert(patch_lines, line)
end end
local patch_content = table.concat(patch_lines, '\n') local patch_content = table.concat(patch_lines, '\n')
-- Write patch to temp file -- Write patch to temp file
local temp_patch = '/tmp/claude-hunk-patch.diff' local temp_patch = '/tmp/claude-hunk-patch.diff'
utils.write_file(temp_patch, patch_content) utils.write_file(temp_patch, patch_content)
-- Apply the patch -- Apply the patch
local git_root = utils.get_project_root() local git_root = utils.get_project_root()
if not git_root then if not git_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR) vim.notify('Not in a git repository', vim.log.levels.ERROR)
return return
end end
local cmd = string.format('cd "%s" && git apply --cached "%s"', git_root, temp_patch) local cmd = string.format('cd "%s" && git apply --cached "%s"', git_root, temp_patch)
local result, err = utils.exec(cmd) local result, err = utils.exec(cmd)
if err then if err then
-- Try without --cached -- Try without --cached
cmd = string.format('cd "%s" && git apply "%s"', git_root, temp_patch) cmd = string.format('cd "%s" && git apply "%s"', git_root, temp_patch)
result, err = utils.exec(cmd) result, err = utils.exec(cmd)
if err then if err then
vim.notify('Failed to apply hunk: ' .. err, vim.log.levels.ERROR) vim.notify('Failed to apply hunk: ' .. err, vim.log.levels.ERROR)
return return
end end
end end
vim.notify(string.format('Applied hunk to %s', hunk_info.file), vim.log.levels.INFO) vim.notify(string.format('Applied hunk to %s', hunk_info.file), vim.log.levels.INFO)
-- Refresh the buffer if it's open -- Refresh the buffer if it's open
vim.cmd('checktime') vim.cmd 'checktime'
end end
return M return M

View File

@ -5,12 +5,7 @@ local M = {}
M.pre_edit_commit = nil M.pre_edit_commit = nil
function M.setup() function M.setup()
-- Auto-cleanup old Claude commits on startup -- Setup persistence layer on startup
vim.defer_fn(function()
M.cleanup_old_commits()
end, 200)
-- Create baseline on startup if we're in a git repo
vim.defer_fn(function() vim.defer_fn(function()
M.create_startup_baseline() M.create_startup_baseline()
end, 500) end, 500)
@ -27,6 +22,7 @@ end
function M.post_tool_use_hook() function M.post_tool_use_hook()
-- Run directly without vim.schedule for testing -- Run directly without vim.schedule for testing
local utils = require 'nvim-claude.utils' local utils = require 'nvim-claude.utils'
local persistence = require 'nvim-claude.inline-diff-persistence'
-- Refresh all buffers to show Claude's changes -- Refresh all buffers to show Claude's changes
vim.cmd 'checktime' vim.cmd 'checktime'
@ -53,26 +49,15 @@ function M.post_tool_use_hook()
end end
end end
-- Get the baseline commit reference first -- Get the stash reference from pre-hook
local baseline_ref = utils.read_file '/tmp/claude-baseline-commit' local stash_ref = persistence.current_stash_ref
if baseline_ref then if not stash_ref then
baseline_ref = baseline_ref:gsub('%s+', '') -- trim whitespace -- If no pre-hook stash, create one now
stash_ref = persistence.create_stash('nvim-claude: changes detected ' .. os.date('%Y-%m-%d %H:%M:%S'))
persistence.current_stash_ref = stash_ref
end end
-- Create a stash of Claude's changes (but keep them in working directory) if stash_ref then
local timestamp = os.date '%Y-%m-%d %H:%M:%S'
local stash_msg = string.format('[claude-edit] %s', timestamp)
-- Use git stash create to create stash without removing changes
local stash_cmd = string.format('cd "%s" && git stash create -u', git_root)
local stash_hash, stash_err = utils.exec(stash_cmd)
if not stash_err and stash_hash and stash_hash ~= '' then
-- Store the stash with a message
stash_hash = stash_hash:gsub('%s+', '') -- trim whitespace
local store_cmd = string.format('cd "%s" && git stash store -m "%s" %s', git_root, stash_msg, stash_hash)
utils.exec(store_cmd)
-- Check if any modified files are currently open in buffers -- Check if any modified files are currently open in buffers
local inline_diff = require 'nvim-claude.inline-diff' local inline_diff = require 'nvim-claude.inline-diff'
local opened_inline = false local opened_inline = false
@ -85,9 +70,9 @@ function M.post_tool_use_hook()
if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf) local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name == full_path or buf_name:match('/' .. file:gsub('([^%w])', '%%%1') .. '$') then if buf_name == full_path or buf_name:match('/' .. file:gsub('([^%w])', '%%%1') .. '$') then
-- Get the original content (from baseline) -- Get the original content from stash
local baseline_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, baseline_ref or 'HEAD', file) local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, stash_ref, file)
local original_content, orig_err = utils.exec(baseline_cmd) local original_content, orig_err = utils.exec(stash_cmd)
if not orig_err and original_content then if not orig_err and original_content then
-- Get current content -- Get current content
@ -116,14 +101,12 @@ function M.post_tool_use_hook()
-- If no inline diff was shown, fall back to regular diff review -- If no inline diff was shown, fall back to regular diff review
if not opened_inline then if not opened_inline then
-- Trigger diff review - show Claude stashes against baseline -- Trigger diff review with stash reference
local ok, diff_review = pcall(require, 'nvim-claude.diff-review') local ok, diff_review = pcall(require, 'nvim-claude.diff-review')
if ok then if ok then
diff_review.handle_claude_stashes(baseline_ref) diff_review.handle_claude_edit(stash_ref, nil)
else
end end
end end
else
end end
end end
@ -132,6 +115,7 @@ function M.test_inline_diff()
vim.notify('Testing inline diff manually...', vim.log.levels.INFO) vim.notify('Testing inline diff manually...', vim.log.levels.INFO)
local utils = require 'nvim-claude.utils' local utils = require 'nvim-claude.utils'
local persistence = require 'nvim-claude.inline-diff-persistence'
local git_root = utils.get_project_root() local git_root = utils.get_project_root()
if not git_root then if not git_root then
@ -163,12 +147,20 @@ function M.test_inline_diff()
if inline_diff.original_content[bufnr] then if inline_diff.original_content[bufnr] then
original_content = inline_diff.original_content[bufnr] original_content = inline_diff.original_content[bufnr]
vim.notify('Using updated baseline from memory (length: ' .. #original_content .. ')', vim.log.levels.INFO) vim.notify('Using updated baseline from memory (length: ' .. #original_content .. ')', vim.log.levels.INFO)
else elseif persistence.current_stash_ref then
-- Fall back to git baseline -- Try to get from stash
local baseline_ref = utils.read_file '/tmp/claude-baseline-commit' or 'HEAD' local stash_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, persistence.current_stash_ref, relative_path)
baseline_ref = baseline_ref:gsub('%s+', '') local git_err
original_content, git_err = utils.exec(stash_cmd)
local baseline_cmd = string.format('cd "%s" && git show %s:%s 2>/dev/null', git_root, baseline_ref, relative_path) if git_err then
vim.notify('Failed to get stash content: ' .. git_err, vim.log.levels.ERROR)
return
end
vim.notify('Using stash baseline: ' .. persistence.current_stash_ref, vim.log.levels.INFO)
else
-- Fall back to HEAD
local baseline_cmd = string.format('cd "%s" && git show HEAD:%s 2>/dev/null', git_root, relative_path)
local git_err local git_err
original_content, git_err = utils.exec(baseline_cmd) original_content, git_err = utils.exec(baseline_cmd)
@ -176,7 +168,7 @@ function M.test_inline_diff()
vim.notify('Failed to get baseline content: ' .. git_err, vim.log.levels.ERROR) vim.notify('Failed to get baseline content: ' .. git_err, vim.log.levels.ERROR)
return return
end end
vim.notify('Using git baseline', vim.log.levels.INFO) vim.notify('Using HEAD as baseline', vim.log.levels.INFO)
end end
-- Get current content -- Get current content
@ -187,65 +179,33 @@ function M.test_inline_diff()
inline_diff.show_inline_diff(bufnr, original_content, current_content) inline_diff.show_inline_diff(bufnr, original_content, current_content)
end end
-- Create baseline on Neovim startup -- Create baseline on Neovim startup (now just sets up persistence)
function M.create_startup_baseline() function M.create_startup_baseline()
local utils = require 'nvim-claude.utils' local persistence = require 'nvim-claude.inline-diff-persistence'
-- Check if we're in a git repository -- Setup persistence autocmds
local git_root = utils.get_project_root() persistence.setup_autocmds()
if not git_root then
return -- Try to restore any saved diffs
end persistence.restore_diffs()
-- Check if we already have a baseline
local baseline_file = '/tmp/claude-baseline-commit'
local existing_baseline = utils.read_file(baseline_file)
-- If we have a valid baseline, keep it
if existing_baseline and existing_baseline ~= '' then
existing_baseline = existing_baseline:gsub('%s+', '')
local check_cmd = string.format('cd "%s" && git rev-parse --verify %s^{commit} 2>/dev/null', git_root, existing_baseline)
local check_result, check_err = utils.exec(check_cmd)
if check_result and not check_err and check_result:match '^%x+' then
-- Baseline is valid
vim.notify('Claude baseline loaded: ' .. existing_baseline:sub(1, 7), vim.log.levels.INFO)
return
end
end
-- Create new baseline of current state
local timestamp = os.time()
local commit_msg = string.format('claude-baseline-%d (startup)', timestamp)
-- Stage all current changes
local add_cmd = string.format('cd "%s" && git add -A', git_root)
utils.exec(add_cmd)
-- Create baseline commit
local commit_cmd = string.format('cd "%s" && git commit -m "%s" --allow-empty', git_root, commit_msg)
local commit_result, commit_err = utils.exec(commit_cmd)
if not commit_err or commit_err:match 'nothing to commit' then
-- Get the commit hash
local hash_cmd = string.format('cd "%s" && git rev-parse HEAD', git_root)
local commit_hash, hash_err = utils.exec(hash_cmd)
if not hash_err and commit_hash then
commit_hash = commit_hash:gsub('%s+', '')
utils.write_file(baseline_file, commit_hash)
vim.notify('Claude baseline created: ' .. commit_hash:sub(1, 7), vim.log.levels.INFO)
end
end
end end
-- Manual hook testing -- Manual hook testing
function M.test_hooks() function M.test_hooks()
vim.notify('=== Testing nvim-claude hooks ===', vim.log.levels.INFO) vim.notify('=== Testing nvim-claude hooks ===', vim.log.levels.INFO)
local persistence = require 'nvim-claude.inline-diff-persistence'
-- Test pre-tool-use hook -- Test creating a stash
vim.notify('1. Testing pre-tool-use hook (creating snapshot)...', vim.log.levels.INFO) vim.notify('1. Creating test stash...', vim.log.levels.INFO)
M.pre_tool_use_hook() local stash_ref = persistence.create_stash('nvim-claude: test stash')
if stash_ref then
persistence.current_stash_ref = stash_ref
vim.notify('Stash created: ' .. stash_ref, vim.log.levels.INFO)
else
vim.notify('Failed to create stash', vim.log.levels.ERROR)
end
-- Simulate making a change -- Simulate making a change
vim.notify('2. Make some changes to test files now...', vim.log.levels.INFO) vim.notify('2. Make some changes to test files now...', vim.log.levels.INFO)
@ -386,6 +346,12 @@ function M.setup_commands()
local inline_diff = require 'nvim-claude.inline-diff' local inline_diff = require 'nvim-claude.inline-diff'
inline_diff.original_content[bufnr] = current_content inline_diff.original_content[bufnr] = current_content
-- Save updated state
local persistence = require 'nvim-claude.inline-diff-persistence'
if persistence.current_stash_ref then
persistence.save_state({ stash_ref = persistence.current_stash_ref })
end
vim.notify('Baseline updated to current buffer state', vim.log.levels.INFO) vim.notify('Baseline updated to current buffer state', vim.log.levels.INFO)
end, { end, {
desc = 'Update Claude baseline to current buffer state', desc = 'Update Claude baseline to current buffer state',
@ -448,15 +414,8 @@ function M.setup_commands()
}) })
end end
-- Cleanup old Claude commits and temp files -- Cleanup old temp files (no longer cleans up commits)
function M.cleanup_old_commits() function M.cleanup_old_files()
local utils = require 'nvim-claude.utils'
local git_root = utils.get_project_root()
if not git_root then
return
end
-- Clean up old temp files -- Clean up old temp files
local temp_files = { local temp_files = {
'/tmp/claude-pre-edit-commit', '/tmp/claude-pre-edit-commit',
@ -470,31 +429,6 @@ function M.cleanup_old_commits()
vim.fn.delete(file) vim.fn.delete(file)
end end
end end
-- Clean up old Claude commits (keep only the last 5)
local log_cmd =
string.format('cd "%s" && git log --oneline --grep="claude-" --grep="claude-baseline" --grep="claude-pre-edit" --all --max-count=10', git_root)
local log_result = utils.exec(log_cmd)
if log_result and log_result ~= '' then
local commits = {}
for line in log_result:gmatch '[^\n]+' do
local hash = line:match '^(%w+)'
if hash then
table.insert(commits, hash)
end
end
-- Keep only the last 5 Claude commits, remove the rest
-- DISABLED: This was causing rebases that broke the workflow
-- if #commits > 5 then
-- for i = 6, #commits do
-- local reset_cmd = string.format('cd "%s" && git rebase --onto %s^ %s', git_root, commits[i], commits[i])
-- utils.exec(reset_cmd)
-- end
-- vim.notify('Cleaned up old Claude commits', vim.log.levels.DEBUG)
-- end
end
end end
return M return M

View File

@ -0,0 +1,266 @@
-- Persistence layer for inline diffs
-- Manages saving/loading diff state across neovim sessions without polluting git history
local M = {}
local utils = require('nvim-claude.utils')
-- State file location
M.state_file = vim.fn.stdpath('data') .. '/nvim-claude-inline-diff-state.json'
-- Save current diff state
function M.save_state(diff_data)
-- Structure:
-- {
-- version: 1,
-- timestamp: <unix_timestamp>,
-- stash_ref: "stash@{0}",
-- files: {
-- "/path/to/file": {
-- original_content: "...",
-- hunks: [...],
-- applied_hunks: {...}
-- }
-- }
-- }
local state = {
version = 1,
timestamp = os.time(),
stash_ref = diff_data.stash_ref,
files = {}
}
-- Collect state from all buffers with active diffs
local inline_diff = require('nvim-claude.inline-diff')
for file_path, bufnr in pairs(inline_diff.diff_files) do
if inline_diff.active_diffs[bufnr] then
local diff = inline_diff.active_diffs[bufnr]
state.files[file_path] = {
original_content = inline_diff.original_content[bufnr],
hunks = diff.hunks,
applied_hunks = diff.applied_hunks or {},
new_content = diff.new_content
}
end
end
-- Save to file
local success, err = utils.write_json(M.state_file, state)
if not success then
vim.notify('Failed to save inline diff state: ' .. err, vim.log.levels.ERROR)
return false
end
return true
end
-- Load saved diff state
function M.load_state()
if not utils.file_exists(M.state_file) then
return nil
end
local state, err = utils.read_json(M.state_file)
if not state then
vim.notify('Failed to load inline diff state: ' .. err, vim.log.levels.ERROR)
return nil
end
-- Validate version
if state.version ~= 1 then
vim.notify('Incompatible inline diff state version', vim.log.levels.WARN)
return nil
end
-- Check if stash still exists
if state.stash_ref then
local cmd = string.format('git stash list | grep -q "%s"', state.stash_ref:gsub("{", "\\{"):gsub("}", "\\}"))
local result = os.execute(cmd)
if result ~= 0 then
vim.notify('Saved stash no longer exists: ' .. state.stash_ref, vim.log.levels.WARN)
M.clear_state()
return nil
end
end
return state
end
-- Clear saved state
function M.clear_state()
if utils.file_exists(M.state_file) then
os.remove(M.state_file)
end
end
-- Restore diffs from saved state
function M.restore_diffs()
local state = M.load_state()
if not state then
return false
end
local inline_diff = require('nvim-claude.inline-diff')
local restored_count = 0
-- Restore diffs for each file
for file_path, file_state in pairs(state.files) do
-- Check if file exists and hasn't changed since the diff was created
if utils.file_exists(file_path) then
-- Read current content
local current_content = utils.read_file(file_path)
-- Check if the file matches what we expect (either original or with applied changes)
-- This handles the case where some hunks were accepted
if current_content then
-- Find or create buffer for this file
local bufnr = vim.fn.bufnr(file_path)
if bufnr == -1 then
-- File not loaded, we'll restore when it's opened
-- Store in a pending restores table
M.pending_restores = M.pending_restores or {}
M.pending_restores[file_path] = file_state
else
-- Restore the diff visualization
inline_diff.original_content[bufnr] = file_state.original_content
inline_diff.diff_files[file_path] = bufnr
inline_diff.active_diffs[bufnr] = {
hunks = file_state.hunks,
new_content = file_state.new_content,
current_hunk = 1,
applied_hunks = file_state.applied_hunks or {}
}
-- Apply visualization
inline_diff.apply_diff_visualization(bufnr)
inline_diff.setup_inline_keymaps(bufnr)
restored_count = restored_count + 1
end
end
end
end
if restored_count > 0 then
vim.notify(string.format('Restored inline diffs for %d file(s)', restored_count), vim.log.levels.INFO)
end
-- Store the stash reference for future operations
M.current_stash_ref = state.stash_ref
return true
end
-- Check for pending restores when a buffer is loaded
function M.check_pending_restore(bufnr)
if not M.pending_restores then
return
end
local file_path = vim.api.nvim_buf_get_name(bufnr)
local file_state = M.pending_restores[file_path]
if file_state then
local inline_diff = require('nvim-claude.inline-diff')
-- Restore the diff for this buffer
inline_diff.original_content[bufnr] = file_state.original_content
inline_diff.diff_files[file_path] = bufnr
inline_diff.active_diffs[bufnr] = {
hunks = file_state.hunks,
new_content = file_state.new_content,
current_hunk = 1,
applied_hunks = file_state.applied_hunks or {}
}
-- Apply visualization
inline_diff.apply_diff_visualization(bufnr)
inline_diff.setup_inline_keymaps(bufnr)
-- Remove from pending
M.pending_restores[file_path] = nil
vim.notify('Restored inline diff for ' .. vim.fn.fnamemodify(file_path, ':~:.'), vim.log.levels.INFO)
end
end
-- Create a stash of current changes (instead of baseline commit)
function M.create_stash(message)
message = message or 'nvim-claude: pre-edit state'
-- Check if there are changes to stash
local status = utils.exec('git status --porcelain')
if not status or status == '' then
-- No changes, but we still need to track current state
-- Create an empty stash by making a tiny change
local temp_file = '.nvim-claude-temp'
utils.write_file(temp_file, 'temp')
utils.exec('git add ' .. temp_file)
utils.exec(string.format('git stash push -m "%s" -- %s', message, temp_file))
os.remove(temp_file)
else
-- Stash all current changes
local cmd = string.format('git stash push -m "%s" --include-untracked', message)
local result, err = utils.exec(cmd)
if err and not err:match('Saved working directory') then
vim.notify('Failed to create stash: ' .. err, vim.log.levels.ERROR)
return nil
end
end
-- Get the stash reference
local stash_list = utils.exec('git stash list -n 1')
if stash_list then
local stash_ref = stash_list:match('^(stash@{%d+})')
return stash_ref
end
return nil
end
-- Setup autocmds for persistence
function M.setup_autocmds()
local group = vim.api.nvim_create_augroup('NvimClaudeInlineDiffPersistence', { clear = true })
-- Save state before exiting vim
vim.api.nvim_create_autocmd('VimLeavePre', {
group = group,
callback = function()
local inline_diff = require('nvim-claude.inline-diff')
-- Only save if there are active diffs
local has_active_diffs = false
for _, diff in pairs(inline_diff.active_diffs) do
if diff then
has_active_diffs = true
break
end
end
if has_active_diffs and M.current_stash_ref then
M.save_state({ stash_ref = M.current_stash_ref })
end
end
})
-- Check for pending restores when buffers are loaded
vim.api.nvim_create_autocmd('BufReadPost', {
group = group,
callback = function(ev)
M.check_pending_restore(ev.buf)
end
})
-- Auto-restore on VimEnter
vim.api.nvim_create_autocmd('VimEnter', {
group = group,
once = true,
callback = function()
-- Delay slightly to ensure everything is loaded
vim.defer_fn(function()
M.restore_diffs()
end, 100)
end
})
end
return M

View File

@ -349,12 +349,14 @@ function M.accept_current_hunk(bufnr)
local hunk = diff_data.hunks[diff_data.current_hunk] local hunk = diff_data.hunks[diff_data.current_hunk]
if not hunk then return end if not hunk then return end
-- Update the baseline to include this accepted change
M.update_baseline_after_accept(bufnr, hunk)
-- Mark as applied (the changes are already in the buffer) -- Mark as applied (the changes are already in the buffer)
diff_data.applied_hunks[diff_data.current_hunk] = true diff_data.applied_hunks[diff_data.current_hunk] = true
-- Update in-memory baseline to current state
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_content = table.concat(current_lines, '\n')
M.original_content[bufnr] = current_content
-- Remove this hunk from the diff data since it's accepted -- Remove this hunk from the diff data since it's accepted
table.remove(diff_data.hunks, diff_data.current_hunk) table.remove(diff_data.hunks, diff_data.current_hunk)
@ -365,6 +367,10 @@ function M.accept_current_hunk(bufnr)
vim.notify(string.format('Accepted hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO) vim.notify(string.format('Accepted hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
-- Save state for persistence
local persistence = require('nvim-claude.inline-diff-persistence')
persistence.save_state({ stash_ref = persistence.current_stash_ref })
if #diff_data.hunks == 0 then if #diff_data.hunks == 0 then
-- No more hunks to review -- No more hunks to review
vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO) vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO)
@ -387,67 +393,17 @@ function M.reject_current_hunk(bufnr)
-- Revert the hunk by applying original content -- Revert the hunk by applying original content
M.revert_hunk_changes(bufnr, hunk) M.revert_hunk_changes(bufnr, hunk)
-- Create a new baseline commit with the rejected changes reverted -- Save the buffer to ensure changes are on disk
local utils = require('nvim-claude.utils') vim.api.nvim_buf_call(bufnr, function()
local git_root = utils.get_project_root() if vim.bo.modified then
vim.cmd('write')
if git_root then
-- Save the buffer to ensure changes are on disk
vim.api.nvim_buf_call(bufnr, function()
if vim.bo.modified then
vim.cmd('write')
vim.notify('Buffer saved', vim.log.levels.INFO)
else
vim.notify('Buffer already saved', vim.log.levels.INFO)
end
end)
-- Stage only the current file (now with hunk reverted)
local file_path = vim.api.nvim_buf_get_name(bufnr)
local relative_path = file_path:gsub('^' .. git_root .. '/', '')
-- Create a new baseline commit with only this file
local timestamp = os.time()
local commit_msg = string.format('claude-baseline-%d (rejected changes)', timestamp)
-- Use git commit with only the specific file
local commit_cmd = string.format('cd "%s" && git add "%s" && git commit -m "%s" -- "%s"',
git_root, relative_path, commit_msg, relative_path)
local commit_result, commit_err = utils.exec(commit_cmd)
vim.notify('Commit command: ' .. commit_cmd, vim.log.levels.DEBUG)
vim.notify('Commit result: ' .. (commit_result or 'nil'), vim.log.levels.DEBUG)
if commit_result and (commit_result:match('1 file changed') or commit_result:match('create mode') or commit_result:match('nothing to commit')) then
-- Get the new commit hash
local hash_cmd = string.format('cd "%s" && git rev-parse HEAD', git_root)
local commit_hash, hash_err = utils.exec(hash_cmd)
if not hash_err and commit_hash then
commit_hash = commit_hash:gsub('%s+', '')
-- Update the baseline file
utils.write_file('/tmp/claude-baseline-commit', commit_hash)
-- Update in-memory baseline
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_content = table.concat(current_lines, '\n')
M.original_content[bufnr] = current_content
if commit_result:match('nothing to commit') then
vim.notify('No changes to commit after rejection, baseline updated', vim.log.levels.INFO)
else
vim.notify('Baseline commit created after rejection: ' .. commit_hash:sub(1, 7), vim.log.levels.INFO)
end
else
vim.notify('Failed to get commit hash: ' .. (hash_err or 'unknown'), vim.log.levels.ERROR)
end
else
vim.notify('Failed to create baseline commit for rejection', vim.log.levels.ERROR)
if commit_err then
vim.notify('Error: ' .. commit_err, vim.log.levels.ERROR)
end
end end
end end)
-- Update in-memory baseline to current state (with rejected changes)
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_content = table.concat(current_lines, '\n')
M.original_content[bufnr] = current_content
-- Remove this hunk from the diff data since it's rejected -- Remove this hunk from the diff data since it's rejected
table.remove(diff_data.hunks, diff_data.current_hunk) table.remove(diff_data.hunks, diff_data.current_hunk)
@ -459,6 +415,10 @@ function M.reject_current_hunk(bufnr)
vim.notify(string.format('Rejected hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO) vim.notify(string.format('Rejected hunk - %d hunks remaining', #diff_data.hunks), vim.log.levels.INFO)
-- Save state for persistence
local persistence = require('nvim-claude.inline-diff-persistence')
persistence.save_state({ stash_ref = persistence.current_stash_ref })
if #diff_data.hunks == 0 then if #diff_data.hunks == 0 then
-- No more hunks to review -- No more hunks to review
vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO) vim.notify('All hunks processed! Closing inline diff.', vim.log.levels.INFO)
@ -612,6 +572,22 @@ function M.close_inline_diff(bufnr, keep_baseline)
M.original_content[bufnr] = nil M.original_content[bufnr] = nil
end end
-- Check if all diffs are closed
local has_active_diffs = false
for _, diff in pairs(M.active_diffs) do
if diff then
has_active_diffs = true
break
end
end
-- If no more active diffs, clear persistence state
if not has_active_diffs then
local persistence = require('nvim-claude.inline-diff-persistence')
persistence.clear_state()
persistence.current_stash_ref = nil
end
vim.notify('Inline diff closed', vim.log.levels.INFO) vim.notify('Inline diff closed', vim.log.levels.INFO)
end end
@ -620,71 +596,11 @@ function M.has_active_diff(bufnr)
return M.active_diffs[bufnr] ~= nil return M.active_diffs[bufnr] ~= nil
end end
-- Update baseline content after accepting a hunk -- Update baseline content after accepting a hunk (deprecated - no longer creates commits)
function M.update_baseline_after_accept(bufnr, hunk) function M.update_baseline_after_accept(bufnr, hunk)
local utils = require('nvim-claude.utils') -- This function is deprecated but kept for compatibility
local git_root = utils.get_project_root() -- The baseline update is now handled directly in accept_current_hunk
vim.notify('update_baseline_after_accept is deprecated', vim.log.levels.DEBUG)
if not git_root then
vim.notify('Not in a git repository', vim.log.levels.ERROR)
return
end
-- Save the buffer to ensure changes are on disk
vim.api.nvim_buf_call(bufnr, function()
if vim.bo.modified then
vim.cmd('write')
vim.notify('Buffer saved', vim.log.levels.INFO)
else
vim.notify('Buffer already saved', vim.log.levels.INFO)
end
end)
-- Stage only the current file
local file_path = vim.api.nvim_buf_get_name(bufnr)
local relative_path = file_path:gsub('^' .. git_root .. '/', '')
-- Create a new baseline commit with only this file
local timestamp = os.time()
local commit_msg = string.format('claude-baseline-%d (accepted changes)', timestamp)
-- Use git commit with only the specific file
local commit_cmd = string.format('cd "%s" && git add "%s" && git commit -m "%s" -- "%s"',
git_root, relative_path, commit_msg, relative_path)
local commit_result, commit_err = utils.exec(commit_cmd)
vim.notify('Commit command: ' .. commit_cmd, vim.log.levels.INFO)
vim.notify('Commit result: ' .. (commit_result or 'nil'), vim.log.levels.INFO)
if commit_result and (commit_result:match('1 file changed') or commit_result:match('create mode') or commit_result:match('nothing to commit')) then
-- Commit was successful or there was nothing to commit (file already at desired state)
local hash_cmd = string.format('cd "%s" && git rev-parse HEAD', git_root)
local commit_hash, hash_err = utils.exec(hash_cmd)
if not hash_err and commit_hash then
commit_hash = commit_hash:gsub('%s+', '')
-- Update the baseline file
utils.write_file('/tmp/claude-baseline-commit', commit_hash)
-- Update in-memory baseline
local current_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_content = table.concat(current_lines, '\n')
M.original_content[bufnr] = current_content
if commit_result:match('nothing to commit') then
vim.notify('No changes to commit, baseline updated to current state', vim.log.levels.INFO)
else
vim.notify('Baseline commit created: ' .. commit_hash:sub(1, 7), vim.log.levels.INFO)
end
else
vim.notify('Failed to get commit hash: ' .. (hash_err or 'unknown'), vim.log.levels.ERROR)
end
else
vim.notify('Failed to create baseline commit', vim.log.levels.ERROR)
if commit_err then
vim.notify('Error: ' .. commit_err, vim.log.levels.ERROR)
end
end
end end
-- Test keymap functionality -- Test keymap functionality