From 92744b538731c0320a220e1fa0dd4749df52471e Mon Sep 17 00:00:00 2001 From: Yanuo Ma Date: Sat, 7 Mar 2026 23:29:34 -0500 Subject: [PATCH 1/4] chore: add CLAUDE.md with project conventions and skill symlink setup Points Claude Code to AGENTS.md for conventions and provides first-use symlink commands to discover skills and agents from .github/ in .claude/skills/. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..60700514 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +See AGENTS.md for project conventions. + +On first use, create symlinks so Claude Code discovers existing skills and agents: +``` +mkdir -p .claude/skills/nvim-e2e-workflow .claude/skills/codediff-developer +ln -sf ../../.github/skills/nvim-e2e-workflow/SKILL.md .claude/skills/nvim-e2e-workflow/SKILL.md +ln -sf ../../.github/agents/codediff-developer.agent.md .claude/skills/codediff-developer/SKILL.md +``` From 7367fe0a048d7f59cc602e4739556bed68c70b3f Mon Sep 17 00:00:00 2001 From: Yanuo Ma Date: Sat, 7 Mar 2026 23:46:43 -0500 Subject: [PATCH 2/4] fix(ui): consolidate discard prompts to use vim.fn.confirm (#310) Replace nvim_echo+getcharstr and vim.ui.select with vim.fn.confirm for all discard confirmations, preventing Noice/Snacks from hijacking the prompt into a disappearing notification. Co-Authored-By: Claude Opus 4.6 --- lua/codediff/ui/auto_refresh.lua | 9 ++++-- lua/codediff/ui/explorer/actions.lua | 19 ++++-------- lua/codediff/ui/explorer/render.lua | 5 +++- lua/codediff/ui/view/keymaps.lua | 43 ++++++++++++++-------------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/lua/codediff/ui/auto_refresh.lua b/lua/codediff/ui/auto_refresh.lua index 15a55880..192225e7 100644 --- a/lua/codediff/ui/auto_refresh.lua +++ b/lua/codediff/ui/auto_refresh.lua @@ -406,9 +406,14 @@ function M.sync_mutable_buffers(tabpage) if #current_lines == #lines then local same = true for i = 1, #lines do - if current_lines[i] ~= lines[i] then same = false; break end + if current_lines[i] ~= lines[i] then + same = false + break + end + end + if same then + return end - if same then return end end local was_modifiable = vim.bo[bufnr].modifiable diff --git a/lua/codediff/ui/explorer/actions.lua b/lua/codediff/ui/explorer/actions.lua index 6c9b3fa3..acb7acce 100644 --- a/lua/codediff/ui/explorer/actions.lua +++ b/lua/codediff/ui/explorer/actions.lua @@ -362,21 +362,12 @@ function M.restore_entry(explorer, tree) local is_untracked = not is_directory and status == "??" local display_name = entry_path .. (is_directory and "/" or "") - -- Two-line confirmation prompt + -- Confirmation prompt local action_word = is_directory and "Discard all changes in " or (is_untracked and "Delete " or "Discard changes to ") - vim.api.nvim_echo({ - { action_word, "WarningMsg" }, - { display_name, "WarningMsg" }, - { "?\n", "WarningMsg" }, - { "(D)", "WarningMsg" }, - { is_untracked and "elete, " or "iscard, ", "WarningMsg" }, - { "[C]", "WarningMsg" }, - { "ancel: ", "WarningMsg" }, - }, false, {}) - - local char = vim.fn.getcharstr():lower() - - if char == "d" then + local prompt = action_word .. display_name .. "?" + local choice = vim.fn.confirm(prompt, "&Discard\n&Cancel", 2, "Warning") + + if choice == 1 then if is_untracked then -- Delete untracked file/directory git.delete_untracked(explorer.git_root, entry_path, function(err) diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index f208cdde..3007f444 100644 --- a/lua/codediff/ui/explorer/render.lua +++ b/lua/codediff/ui/explorer/render.lua @@ -339,7 +339,10 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target if current_status then local file_has_staged = false for _, sf in ipairs(current_status.staged or {}) do - if sf.path == file_path then file_has_staged = true; break end + if sf.path == file_path then + file_has_staged = true + break + end end local current_is_mutable = session.original_revision and session.original_revision:match("^:[0-3]$") if file_has_staged ~= (current_is_mutable and true or false) then diff --git a/lua/codediff/ui/view/keymaps.lua b/lua/codediff/ui/view/keymaps.lua index 544ca80c..546bef0c 100644 --- a/lua/codediff/ui/view/keymaps.lua +++ b/lua/codediff/ui/view/keymaps.lua @@ -520,32 +520,31 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore end -- Prompt for confirmation before discarding (destructive operation) - local prompt = string.format("Discard hunk %d? This cannot be undone.", hunk_idx) - vim.ui.select({ "Yes", "No" }, { prompt = prompt }, function(choice) - if choice ~= "Yes" then - return - end + local prompt = string.format("Discard hunk %d?", hunk_idx) + local choice = vim.fn.confirm(prompt, "&Discard\n&Cancel", 2, "Warning") + if choice ~= 1 then + return + end - local discard_orig_buf, discard_mod_buf = lifecycle.get_buffers(tabpage) - if not discard_orig_buf or not discard_mod_buf or not vim.api.nvim_buf_is_valid(discard_orig_buf) or not vim.api.nvim_buf_is_valid(discard_mod_buf) then - vim.notify("Diff buffers are no longer available", vim.log.levels.WARN) - return - end + local discard_orig_buf, discard_mod_buf = lifecycle.get_buffers(tabpage) + if not discard_orig_buf or not discard_mod_buf or not vim.api.nvim_buf_is_valid(discard_orig_buf) or not vim.api.nvim_buf_is_valid(discard_mod_buf) then + vim.notify("Diff buffers are no longer available", vim.log.levels.WARN) + return + end - -- Read lines from both buffers for this hunk - local orig_lines = vim.api.nvim_buf_get_lines(discard_orig_buf, hunk.original.start_line - 1, hunk.original.end_line - 1, false) - local mod_lines = vim.api.nvim_buf_get_lines(discard_mod_buf, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) + -- Read lines from both buffers for this hunk + local orig_lines = vim.api.nvim_buf_get_lines(discard_orig_buf, hunk.original.start_line - 1, hunk.original.end_line - 1, false) + local mod_lines = vim.api.nvim_buf_get_lines(discard_mod_buf, hunk.modified.start_line - 1, hunk.modified.end_line - 1, false) - local patch = build_hunk_patch(file_path, orig_lines, mod_lines, hunk.original.start_line, hunk.modified.start_line) + local patch = build_hunk_patch(file_path, orig_lines, mod_lines, hunk.original.start_line, hunk.modified.start_line) - local git = require("codediff.core.git") - git.discard_hunk_patch(session.git_root, patch, function(err) - if err then - vim.notify("Failed to discard hunk: " .. err, vim.log.levels.ERROR) - return - end - vim.notify(string.format("Discarded hunk %d", hunk_idx), vim.log.levels.INFO) - end) + local git = require("codediff.core.git") + git.discard_hunk_patch(session.git_root, patch, function(err) + if err then + vim.notify("Failed to discard hunk: " .. err, vim.log.levels.ERROR) + return + end + vim.notify(string.format("Discarded hunk %d", hunk_idx), vim.log.levels.INFO) end) end From 0e7feeb2ae4e92c931e8702e7cf83f3a491ce3ba Mon Sep 17 00:00:00 2001 From: Yanuo Ma Date: Sat, 7 Mar 2026 23:59:40 -0500 Subject: [PATCH 3/4] feat(navigation): center cursor after hunk navigation (#290) Add zz after ]c and [c to center the target hunk on screen, matching VSCode behavior. Co-Authored-By: Claude Opus 4.6 --- lua/codediff/ui/view/navigation.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/codediff/ui/view/navigation.lua b/lua/codediff/ui/view/navigation.lua index 1b84c51f..745b20c3 100644 --- a/lua/codediff/ui/view/navigation.lua +++ b/lua/codediff/ui/view/navigation.lua @@ -52,6 +52,7 @@ function M.next_hunk() local target_line = is_original and mapping.original.start_line or mapping.modified.start_line if target_line > current_line then pcall(vim.api.nvim_win_set_cursor, 0, { target_line, 0 }) + vim.cmd("normal! zz") vim.api.nvim_echo({ { string.format("Hunk %d of %d", i, #diff_result.changes), "None" } }, false, {}) return true end @@ -62,6 +63,7 @@ function M.next_hunk() local first_hunk = diff_result.changes[1] local target_line = is_original and first_hunk.original.start_line or first_hunk.modified.start_line pcall(vim.api.nvim_win_set_cursor, 0, { target_line, 0 }) + vim.cmd("normal! zz") vim.api.nvim_echo({ { string.format("Hunk 1 of %d", #diff_result.changes), "None" } }, false, {}) return true else @@ -119,6 +121,7 @@ function M.prev_hunk() local target_line = is_original and mapping.original.start_line or mapping.modified.start_line if target_line < current_line then pcall(vim.api.nvim_win_set_cursor, 0, { target_line, 0 }) + vim.cmd("normal! zz") vim.api.nvim_echo({ { string.format("Hunk %d of %d", i, #diff_result.changes), "None" } }, false, {}) return true end @@ -129,6 +132,7 @@ function M.prev_hunk() local last_hunk = diff_result.changes[#diff_result.changes] local target_line = is_original and last_hunk.original.start_line or last_hunk.modified.start_line pcall(vim.api.nvim_win_set_cursor, 0, { target_line, 0 }) + vim.cmd("normal! zz") vim.api.nvim_echo({ { string.format("Hunk %d of %d", #diff_result.changes, #diff_result.changes), "None" } }, false, {}) return true else From f62e5174ff7648ccf776667d011cdf8613069cd6 Mon Sep 17 00:00:00 2001 From: Yanuo Ma Date: Sun, 8 Mar 2026 00:46:37 -0500 Subject: [PATCH 4/4] fix(test): update discard_hunk test to mock vim.fn.confirm The #310 change switched discard prompts from vim.ui.select to vim.fn.confirm, but the test still mocked vim.ui.select. Update the mock to return 1 (Discard) from vim.fn.confirm instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/ui/explorer/hunk_operations_spec.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/ui/explorer/hunk_operations_spec.lua b/tests/ui/explorer/hunk_operations_spec.lua index 08120af9..d98496d9 100644 --- a/tests/ui/explorer/hunk_operations_spec.lua +++ b/tests/ui/explorer/hunk_operations_spec.lua @@ -393,11 +393,10 @@ describe("Hunk operations (side-by-side)", function() local discard_fn = get_keymap_fn(mod_buf, "Discard hunk") assert.is_truthy(discard_fn, "discard_hunk keymap callback should be set") - -- Mock vim.ui.select to auto-confirm "Yes" - local original_ui_select = vim.ui.select - vim.ui.select = function(items, opts, on_choice) - -- Auto-confirm with "Yes" - on_choice("Yes") + -- Mock vim.fn.confirm to auto-confirm "Discard" (choice 1) + local original_confirm = vim.fn.confirm + vim.fn.confirm = function(_, _, _, _) + return 1 end -- Position cursor on hunk 1 (line 1) @@ -406,8 +405,8 @@ describe("Hunk operations (side-by-side)", function() -- Invoke discard_hunk discard_fn() - -- Restore original vim.ui.select - vim.ui.select = original_ui_select + -- Restore original vim.fn.confirm + vim.fn.confirm = original_confirm -- Wait for the async git operation to complete vim.wait(2000, function() return false end, 100)