Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,9 @@ jobs:

- name: Run integration tests
run: ./scripts/run_integration_tests_individually.sh

# Real-Neovim regression gate for issue #238 (reject-with-:q). The unit spec
# only exercises the WinClosed handler logic against a mock; this drives a
# genuine `:q` and self-reports via cquit (exit 0 = fixed, 1 = regressed).
- name: Reject-on-quit regression gate (#238)
run: nvim --headless -u NONE -l scripts/repro_issue_238.lua
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Bug Fixes

- `focus_after_send = true` no longer fails silently with `terminal.provider = "none"`/`"external"`: those providers run Claude outside Neovim, so focus cannot move there. A one-time warning is now emitted at setup pointing to the new `User ClaudeCodeSendComplete` autocmd, which you can hook to focus your own terminal. (`focus_after_send` still only auto-focuses the in-editor providers.) ([#228](https://github.com/coder/claudecode.nvim/issues/228))
- Rejecting a Claude diff with `:q` (or `:close` / `<C-w>c` / closing the tab) now resolves it as rejected, matching the documented behavior. The proposed buffer is a scratch buffer that `:q` only hides, so the existing `BufDelete`/`BufUnload`/`BufWipeout` autocmds never fired; a `WinClosed` autocmd now handles window-close rejection. ([#238](https://github.com/coder/claudecode.nvim/issues/238))
- Diffs opened via `openDiff` no longer linger forever when they are resolved outside this Neovim or their Claude session goes away. Pending diffs are now automatically closed when the client that opened them disconnects or the integration is stopped, and `closeAllDiffTabs` now also resolves/cleans the diff module's tracked state instead of only closing windows. ([#248](https://github.com/coder/claudecode.nvim/issues/248))
- Show diffs when the Claude Code terminal is the only window (no other splits). Previously `openDiff` failed with "No suitable editor window found"; now a split is created to host the diff, matching the behavior of the `openFile` tool. ([#231](https://github.com/coder/claudecode.nvim/issues/231))
- Work around a Neovim core bug (< 0.12.2) that fragmented large bracketed pastes into the terminal across `vim.paste` phases, making Cmd+V appear to truncate content. Added a scoped, version-gated `vim.paste` shim controlled by `terminal.fix_streamed_paste` (`"auto"` by default; no-op on Neovim >= 0.12.2). ([#161](https://github.com/coder/claudecode.nvim/issues/161))
Expand Down
91 changes: 91 additions & 0 deletions fixtures/issue-238/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# `issue-238` fixture — repro for issue #238

Reproduces [#238 "[BUG] Rejecting with `:q` does not work"](https://github.com/coder/claudecode.nvim/issues/238):
the README documents two ways to reject a Claude diff — `:q` **or** `<leader>ad`
(`:ClaudeCodeDiffDeny`). The keymap works; **`:q` does not reject**. The proposed
window closes but Claude is never told `DIFF_REJECTED`, and (with
`open_in_new_tab = true`) the diff tab lingers.

This fixture uses the reporter's exact config:

- `terminal.provider = "none"` (Claude runs in an external terminal, e.g.
sidekick.nvim, so claudecode manages no terminal of its own), and
- `diff_opts = { layout = "vertical", open_in_new_tab = true }`.

It mirrors [`remote-diff`](../remote-diff) but adds a JSON `:DiffStateFile`
inspector that records window/tab counts, each diff's status, **and the proposed
buffer's `bufhidden`** — the smoking gun for this bug.

> Set `REPRO238_NEW_TAB=0` to launch in the **default** same-tab layout and
> confirm the bug is not tab-specific (it reproduces there too — the split just
> silently collapses to the original file).

## Files

- `init.lua` — claudecode.nvim config matching the issue + `:DiffState`/`:DiffStateFile`.
- `example/target.txt` — a sample file to diff against.

## Root cause (verified)

The proposed buffer is created with `vim.api.nvim_create_buf(false, true)`
(a scratch buffer ⇒ `bufhidden = "hide"`) and rejection is wired **only** through
buffer-destruction autocmds (`BufDelete` / `BufUnload` / `BufWipeout` →
`_resolve_diff_as_rejected`). Because `bufhidden = "hide"`, `:q` merely **hides**
the still-loaded buffer instead of destroying it, so none of those autocmds fire
and the diff is never resolved. (`:ClaudeCodeDiffDeny` works because it calls
`_resolve_diff_as_rejected` directly, bypassing the autocmds.)

## Quick start — headless one-liner (no WebSocket needed)

The fastest way to see the bug. Drives the real `diff.lua` and performs a genuine
`:q`, for both `open_in_new_tab` layouts:

```sh
nvim --headless -u NONE -l scripts/repro_issue_238.lua; echo "exit: $?"
```

Exit code **1** = bug reproduced (current code), **0** = fixed. On current code it prints:

```
[default config (open_in_new_tab=false)]
proposed buffer bufhidden = "hide"
after :q -> rejected=false status=pending proposed_buf_still_loaded=true tabpages 1->1
=> BUG: `:q` did NOT reject the diff (Claude never receives DIFF_REJECTED)
[reporter config (open_in_new_tab=true)]
proposed buffer bufhidden = "hide"
after :q -> rejected=false status=pending proposed_buf_still_loaded=true tabpages 2->2
=> BUG: ...
```

## Quick start — live, playing the role of Claude over MCP

```sh
# Terminal 1 — the editor under test:
source fixtures/nvim-aliases.sh
vv issue-238 example/target.txt
# (equivalently: NVIM_APPNAME=issue-238 XDG_CONFIG_HOME=fixtures nvim fixtures/issue-238/example/target.txt)
# The server auto-starts; check the lock file: ls ~/.claude/ide/*.lock

# Terminal 2 — open a diff and HOLD the socket open while you reject in Neovim:
scripts/repro_issue_238.sh --file "$PWD/fixtures/issue-238/example/target.txt" --hold 30
```

A diff opens in a new tab. In Neovim, try to reject it with **`:q`**. Then:

- Run `:DiffState` in Neovim → it still shows the diff as `[pending]`
(`bufhidden=hide`), and the tab is still open.
- The Terminal-2 script reports `no DIFF_REJECTED / FILE_SAVED in window` — Claude
never learned the diff was rejected.

Contrast: reject with `:ClaudeCodeDiffDeny` (or `<leader>ad`) instead → the script
prints `DIFF_REJECTED was received` and `:DiffState` shows the diff `[rejected]`.

> If `websocat` is a `mise` shim that refuses to run in this directory, pass the
> real binary: `WEBSOCAT="$(mise which websocat)" scripts/repro_issue_238.sh ...`.

## Inspector commands (added by this fixture)

- `:DiffState` — notify window/tab count + each active diff's status, `created_new_tab`, and proposed `bufhidden`.
- `:DiffStateFile [path]` — write the same info as JSON (for automation; defaults to `stdpath('cache')/diff_state.json`).
- `<leader>as` — run `:DiffState`.
- `<leader>aa` / `<leader>ad` — accept / deny the focused diff.
5 changes: 5 additions & 0 deletions fixtures/issue-238/example/target.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
line one
line two
line three
line four
line five
143 changes: 143 additions & 0 deletions fixtures/issue-238/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
-- Repro fixture for issue #238: "[BUG] Rejecting with `:q` does not work".
--
-- Scenario this fixture is built to demonstrate:
-- 1. claudecode.nvim is configured exactly like the reporter:
-- - terminal.provider = "none" (Claude runs in an *external* terminal,
-- e.g. sidekick.nvim — Neovim manages no terminal of its own)
-- - diff_opts.open_in_new_tab = true
-- - diff_opts.layout = "vertical"
-- 2. Claude opens a diff via the `openDiff` MCP tool. It lands in a NEW tab
-- with the original file on the left and the proposed buffer on the right.
-- 3. The user tries to REJECT the change with `:q` (as the README documents:
-- "Reject: `:q` or <leader>ad").
-- 4. EXPECTED: the diff is rejected (Claude is told DIFF_REJECTED) and the
-- tab closes.
-- ACTUAL (the bug): `:q` only closes the proposed window; the buffer is
-- merely *hidden* (it is a scratch buffer => bufhidden=hide), so none of
-- the BufDelete/BufUnload/BufWipeout autocmds that drive rejection fire.
-- The diff stays "pending" forever and the tab lingers.
--
-- This fixture mirrors `remote-diff` but uses the reporter's exact config and
-- exposes a `:DiffStateFile` command that writes a machine-readable JSON
-- snapshot (window/tab counts, per-diff status, and the proposed buffer's
-- bufhidden) so automation can assert on the bug without scraping the screen.
--
-- Usage (from repo root):
-- source fixtures/nvim-aliases.sh
-- vv issue-238 example/target.txt
-- # or: NVIM_APPNAME=issue-238 XDG_CONFIG_HOME=fixtures nvim fixtures/issue-238/example/target.txt
--
-- Then drive the MCP side (play the role of Claude) with:
-- scripts/repro_issue_238.sh

local config_dir = vim.fn.stdpath("config")
local repo_root = vim.fn.fnamemodify(config_dir, ":h:h")
vim.opt.rtp:prepend(repo_root)

vim.g.mapleader = " "
vim.g.maplocalleader = "\\"

local ok, claudecode = pcall(require, "claudecode")
assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode))

-- The reporter's exact config uses open_in_new_tab = true, but the underlying
-- bug is not tab-specific. Set REPRO238_NEW_TAB=0 to probe the default
-- (same-tab) layout and confirm `:q` rejection is broken there too.
local open_in_new_tab = os.getenv("REPRO238_NEW_TAB") ~= "0"

claudecode.setup({
auto_start = false,
-- Quiet logging keeps the diff UI clean for screenshots / automation and
-- avoids the hit-enter prompt that long :messages can trigger.
log_level = "warn",
terminal = {
-- The reporter uses sidekick.nvim to run Claude in an external terminal,
-- so claudecode itself manages no terminal: provider = "none".
provider = "none",
},
diff_opts = {
layout = "vertical",
open_in_new_tab = open_in_new_tab,
keep_terminal_focus = false,
},
})

local function ensure_started()
local ok_start, started_or_err, port_or_err = pcall(function()
return claudecode.start(false)
end)
if not ok_start then
vim.notify("ClaudeCode start crashed: " .. tostring(started_or_err), vim.log.levels.ERROR)
return false
end
if started_or_err or port_or_err == "Already running" then
return true
end
vim.notify("ClaudeCode failed to start: " .. tostring(port_or_err), vim.log.levels.ERROR)
return false
end

ensure_started()

-- Build a snapshot of everything that matters for this bug.
local function diff_state()
local diff = require("claudecode.diff")
local active = diff._get_active_diffs()

local diffs = {}
for tab_name, data in pairs(active) do
local proposed_bufhidden = nil
if data.new_buffer and vim.api.nvim_buf_is_valid(data.new_buffer) then
proposed_bufhidden = vim.api.nvim_buf_get_option(data.new_buffer, "bufhidden")
end
diffs[#diffs + 1] = {
tab_name = tab_name,
status = data.status or "?",
created_new_tab = data.created_new_tab or false,
new_buffer = data.new_buffer,
new_buffer_valid = data.new_buffer and vim.api.nvim_buf_is_valid(data.new_buffer) or false,
new_buffer_loaded = data.new_buffer and vim.api.nvim_buf_is_loaded(data.new_buffer) or false,
proposed_bufhidden = proposed_bufhidden,
}
end
table.sort(diffs, function(a, b)
return tostring(a.tab_name) < tostring(b.tab_name)
end)

return {
windows = #vim.api.nvim_list_wins(),
tabpages = #vim.api.nvim_list_tabpages(),
active_diffs = #diffs,
diffs = diffs,
}
end

-- Human-readable variant.
vim.api.nvim_create_user_command("DiffState", function()
local s = diff_state()
local lines = {
("windows=%d tabpages=%d active_diffs=%d"):format(s.windows, s.tabpages, s.active_diffs),
}
for _, d in ipairs(s.diffs) do
lines[#lines + 1] = (" [%s] new_tab=%s bufhidden=%s loaded=%s %s"):format(
d.status,
tostring(d.created_new_tab),
tostring(d.proposed_bufhidden),
tostring(d.new_buffer_loaded),
d.tab_name
)
end
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
end, { desc = "Show window/tab count + active claudecode diffs" })

-- Scriptable variant: writes the state as JSON to a file so external automation
-- can assert on it without scraping the message area.
vim.api.nvim_create_user_command("DiffStateFile", function(opts)
local path = opts.args ~= "" and opts.args or (vim.fn.stdpath("cache") .. "/diff_state.json")
local s = diff_state()
vim.fn.writefile({ vim.json.encode(s) }, path)
end, { nargs = "?", desc = "Write window/diff state as JSON to a file" })

vim.keymap.set("n", "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", { desc = "Accept diff" })
vim.keymap.set("n", "<leader>ad", "<cmd>ClaudeCodeDiffDeny<cr>", { desc = "Deny diff" })
vim.keymap.set("n", "<leader>as", "<cmd>DiffState<cr>", { desc = "Show diff state" })
36 changes: 36 additions & 0 deletions lua/claudecode/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,39 @@ local function register_diff_autocmds(tab_name, new_buffer)
end,
})

-- WinClosed: reject when the proposed window is closed (`:q`, `:close`, `<C-w>c`, `:tabclose`).
-- The proposed buffer is scratch (bufhidden="hide"), so closing its window merely HIDES the
-- still-loaded buffer and none of BufDelete/BufUnload/BufWipeout below fire -> `:q` would never
-- resolve the diff and Claude would never receive DIFF_REJECTED (issue #238).
--
-- We do NOT bind to a single window-id pattern: the proposed buffer may be split into several
-- windows (<C-w>v), and rejecting just because the *tracked* window closed would prematurely
-- reject while the user still views a clone (and closing a clone would never match the pattern).
-- Instead we reject only once the proposed buffer is displayed in NO window (across all tabs;
-- a copy the user split into another tab defers rejection until that copy is closed too).
-- WinClosed fires BEFORE the closing window leaves the layout, so we exclude it (args.match).
-- _resolve_diff_as_rejected no-ops once status != "pending", so this is harmless after :w
-- (accept) or during _cleanup_diff_state (which deletes these autocmds before closing windows).
autocmd_ids[#autocmd_ids + 1] = vim.api.nvim_create_autocmd("WinClosed", {
group = get_autocmd_group(),
callback = function(args)
if not vim.api.nvim_buf_is_valid(new_buffer) then
return
end
local closing_win = tonumber(args.match)
local still_visible = false
for _, win in ipairs(vim.fn.win_findbuf(new_buffer)) do
if win ~= closing_win then
still_visible = true
break
end
end
if not still_visible then
M._resolve_diff_as_rejected(tab_name)
end
end,
})
Comment on lines +883 to +901

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The new WinClosed handler at lua/claudecode/diff.lua:883-901 falsely rejects pending diffs whenever the proposed buffer was previously navigated away from (e.g. via :e other_file, :b other_buf, or a file picker like snacks/telescope opening a file in the proposed window). Because the proposed buffer is a scratch buffer (bufhidden="hide"), navigating away merely hides it — none of BufDelete/BufUnload/BufWipeout fire, and the diff stays status="pending". Then any later unrelated WinClosed in the editor (a help split, a sidebar, a tabclose) makes win_findbuf(new_buffer) return {}, the still_visible check fails, and _resolve_diff_as_rejected sends a spurious DIFF_REJECTED to Claude — and on the next close_tab the proposed buffer carrying Claude'''s edits is silently wiped. Fix: track whether the closing window itself was showing the proposed buffer, and reject only when it was AND no other window is.

Extended reasoning...

The bug

The WinClosed autocmd registered in register_diff_autocmds (lua/claudecode/diff.lua:883-901) is intentionally window-wide (no buffer=/pattern= scope) — every WinClosed anywhere in the editor invokes the callback for every active diff. The handler is supposed to reject the diff iff the close was the proposed buffer'''s last visible window. But the check it implements — "after excluding args.match, is the buffer still in any window?" — conflates two scenarios that win_findbuf reports identically when the result is {}:

  1. (a) The closing window was the last one showing the proposed buffer (legitimate reject).
  2. (b) The proposed buffer was already in no window because the user navigated away earlier (must not reject — the close has nothing to do with this diff).

The handler'''s own comment at diff.lua:880 says "WinClosed fires BEFORE the closing window leaves the layout, so we exclude it (args.match)". That nails down case (a)'''s semantics: for a legitimate last-window close, win_findbuf(new_buffer) returns {closing_win}. So win_findbuf returning {} is exclusively case (b) — the bug path.

How it triggers

The proposed buffer is created at diff.lua:1267 via vim.api.nvim_create_buf(false, true). The scratch flag sets bufhidden="hide", and the subsequent buf_set_option("buftype", "acwrite") does not change bufhidden — the PR'''s own fixture (fixtures/issue-238/init.lua) explicitly captures bufhidden and confirms "hide". So when the user runs :e other_file / :b other_buf in the proposed window, or a file picker (snacks-picker default, common telescope/fzf-lua configs) opens a file into the current window, the proposed buffer becomes hidden but stays loaded. None of BufDelete/BufUnload/BufWipeout fires; the diff stays status="pending". Then any unrelated WinClosed anywhere triggers the spurious rejection.

Step-by-step proof

  1. User opens a Claude diff. The proposed buffer is buffer 42, scratch, bufhidden="hide", displayed in window 1001. Diff state: status="pending".
  2. User opens a file picker that opens into the current window (e.g. snacks-picker, telescope select_default), or just runs :b some_other_buffer in window 1001. Window 1001 now shows a different buffer; buffer 42 is hidden but still loaded. No autocmd fires; diff status stays "pending".
  3. Later, the user closes any unrelated window — say a help window with id 9999. WinClosed fires with args.match="9999".
  4. Inside the callback (the active diff'''s registered WinClosed):
    • nvim_buf_is_valid(42)true (still loaded, just hidden).
    • closing_win = 9999.
    • vim.fn.win_findbuf(42){} (buffer 42 is shown in zero windows).
    • The for loop body never executes; still_visible remains false.
    • if not still_visible then M._resolve_diff_as_rejected(tab_name) runs.
  5. _resolve_diff_as_rejected sees status=="pending" and proceeds: it builds a DIFF_REJECTED payload and resumes the deferred coroutine with it. Claude is told the diff was rejected. On the next close_tab cleanup, _cleanup_diff_state wipes buffer 42 — silently destroying Claude'''s proposed edits.

Why the existing guards do not catch this

  • The status~="pending" guard inside _resolve_diff_as_rejected doesn'''t fire — status is still "pending" (no other resolution happened).
  • nvim_buf_is_valid(new_buffer) only checks existence; it returns true for hidden-but-loaded buffers.
  • The PR has no other BufHidden/BufWinLeave autocmd to mark the diff as user-abandoned.

Why this snuck past the tests

The first unit test in tests/unit/diff_reject_on_quit_spec.lua ("rejects the diff when the proposed window closes and the buffer is no longer visible", lines 80–92) locks in this bug. It mocks win_findbuf={} while args.match="1001" and asserts rejection. But per the comment on diff.lua:880 (which the very next test cites: "WinClosed fires BEFORE the window leaves the layout, so win_findbuf still lists the closing window"), the legitimate last-window-close case is the one the second test models — win_findbuf={1001}. win_findbuf={} only models case (b) — the user-navigated-away scenario — and the test asserts that this rejects. So the test name implies case (a) but the mock encodes case (b), and that case (b) is precisely the bug.

Fix

Track whether the closing window itself was showing the proposed buffer, and reject only when it was AND no other window is:

callback = function(args)
  if not vim.api.nvim_buf_is_valid(new_buffer) then return end
  local closing_win = tonumber(args.match)
  local was_in_closing, still_visible_elsewhere = false, false
  for _, win in ipairs(vim.fn.win_findbuf(new_buffer)) do
    if win == closing_win then
      was_in_closing = true
    else
      still_visible_elsewhere = true
    end
  end
  if was_in_closing and not still_visible_elsewhere then
    M._resolve_diff_as_rejected(tab_name)
  end
end,

Equivalently, check vim.api.nvim_win_get_buf(closing_win) == new_buffer while the window still exists (WinClosed fires before layout change, so it does). Either approach makes the spurious-close case (b) a no-op while preserving the legitimate case (a) — and the existing tests for split-still-visible and unrelated-window-no-reject keep passing. The first unit test should then be updated to model the legitimate case (win_findbuf={1001}) and asserts the same rejection.

Comment on lines +883 to +901

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Auto-teardown on client disconnect (close_diffs_for_client at diff.lua:1601) and on server stop (close_pending_diffs at diff.lua:1587) only match status=="pending", so a rejected-but-not-yet-cleaned-up diff is skipped — if Claude disconnects in the window between DIFF_REJECTED being sent and close_tab being received, the new tab + proposed buffer linger until VimLeavePre. However, this is a pre-existing design choice, not a regression introduced by this PR: deny_current_diff at diff.lua:1613-1624 has always called _resolve_diff_as_rejected directly with no UI cleanup, so a user pressing <leader>ad and then having Claude disconnect already hit the same orphan tab pre-PR. The PR makes :q consistent with <leader>ad; it does not change the disconnect-cleanup design. A separate fix relaxing the filters to status ~= "saved" would help all three rejection paths equally.

Extended reasoning...

Analysis

The synthesis describes a real behavioral gap: after this PR, :q synchronously transitions a diff to status="rejected" via the new WinClosed handler (diff.lua:883-901), but the auto-teardown filters in close_diffs_for_client (diff.lua:1601) and close_pending_diffs (diff.lua:1587) only match status=="pending". If Claude disconnects after :q but before sending close_tab, the rejected diff is skipped and the new tab + proposed buffer linger until VimLeavePre. The reporters terminal.provider="none" config makes the trigger plausible: user types :q, then closes the external Claude CLI before Claudes runtime can respond with close_tab.

Why this is pre-existing, not a regression

The refuting verifier makes a load-bearing point: deny_current_diff (diff.lua:1613-1624) has always called _resolve_diff_as_rejected directly and left UI cleanup to Claude sending close_tab. The functions explicit comment is "Do not close windows/tabs here; just mark as rejected." So pre-PR, a user pressing <leader>ad / :ClaudeCodeDiffDeny and then having Claude disconnect already hit exactly the same orphan-tab scenario the synthesis describes. The disconnect filter excluding "rejected" diffs is a pre-existing design choice, not anything this PR introduced.

The synthesis frames this as a regression by comparing against pre-PR :q, which left status="pending" precisely because it failed to reject — the very bug this PR fixes. Restoring that "accidental cleanup" path would mean re-breaking #238. This PR is purely additive on top of an existing state machine: it makes :q honor the same rejection contract <leader>ad already did, including the same disconnect gap.

Step-by-step proof (pre-existing reachability via <leader>ad)

  1. With this PR or without it, configure terminal.provider="none", diff_opts.open_in_new_tab=true.
  2. Claude calls openDiff → new tab with original + proposed windows. Status is "pending".
  3. User presses <leader>addeny_current_diff_resolve_diff_as_rejected → status="rejected", DIFF_REJECTED sent.
  4. Before Claude sends close_tab, user closes the external Claude CLI (a natural action).
  5. WebSocket drops → close_diffs_for_client runs → filter at diff.lua:1601 requires status=="pending" → skips the rejected diff.
  6. New tab + proposed buffer linger until VimLeavePre.

This reproduces identically on main pre-PR, demonstrating the bug is in the disconnect-cleanup filter design (or in deny_current_diff/_resolve_diff_as_rejected not eagerly cleaning up), not in the PRs new WinClosed handler.

Impact and fix

The fix described in the synthesis (relax filters to status ~= "saved", or explicitly include "rejected") would correctly address the underlying issue and benefit all three rejection paths (:w-rollback edge cases aside — the comment at diff.lua:1592-1597 only justifies excluding "saved" to preserve user :w-edits Claude hasnt written yet; "rejected" carries no accepted content). But that fix belongs to a separate PR addressing the pre-existing design, not this one. Filing it as a blocking issue against this PR overstates the change: this PR did not introduce the gap, it only made one more entry point reach it.

Flagging as pre-existing so the maintainer can prioritize the disconnect-filter fix independently if desired, without blocking the #238 fix.


-- Buffer deletion monitoring for rejection (multiple events to catch all deletion methods)

-- BufDelete: When buffer is deleted with :bdelete, :bwipeout, etc.
Expand Down Expand Up @@ -902,6 +935,9 @@ local function register_diff_autocmds(tab_name, new_buffer)
return autocmd_ids
end

-- Exposed for testing the reject-on-window-close (WinClosed) behavior.
M._register_diff_autocmds = register_diff_autocmds

---Create diff view from a specific window
---@param target_window NvimWin|nil The window to use as base for the diff
---@param old_file_path string Path to the original file
Expand Down
Loading
Loading