-
Notifications
You must be signed in to change notification settings - Fork 198
fix(diff): scope closeAllDiffTabs to own diffs; never reuse &diff windows (#277) #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f1771dc
fix(diff): scope closeAllDiffTabs to own diffs; never reuse &diff win…
ThomasK33 8641c21
fix(diff): isolate openDiff in a new tab when the current tab is all …
ThomasK33 cbe95fc
fix(diff): route openDiff to a new tab whenever the destination tab a…
ThomasK33 294873b
fix(open_file): clear diff on the fallback split so an opened file ne…
ThomasK33 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # Fixture: issue #277 — closeAllDiffTabs destroys foreign diffs | ||
|
|
||
| Reproduction environment for | ||
| [#277 "[BUG] closeAllDiffTabs closes all diff-mode windows, destroying unrelated diffs (diffview.nvim)"](https://github.com/coder/claudecode.nvim/issues/277). | ||
|
|
||
| Two defects: | ||
|
|
||
| 1. `tools/close_all_diff_tabs.lua` closes **every** window with `&diff` set (and | ||
| force-deletes `%.diff$` / `diff://` / `fugitive://` buffers) with no check | ||
| that claudecode created them. The Claude CLI invokes `closeAllDiffTabs` at | ||
| the **start of each user turn** when an IDE is connected (verified against | ||
| CLI 2.1.175), so any diffview.nvim / fugitive / native vimdiff layout that is | ||
| open when you submit a prompt gets destroyed. | ||
| 2. `find_main_editor_window()` (in `tools/open_file.lua` and `diff.lua`) does | ||
| not exclude `&diff` windows, so `openFile`/`openDiff` can `:edit` into one | ||
| half of a foreign diff, corrupting it (the new buffer joins the diff). | ||
|
|
||
| ## Scripted reproduction | ||
|
|
||
| ```bash | ||
| scripts/repro_issue_277.sh | ||
| ``` | ||
|
|
||
| Drives a real Neovim TUI under agent-tty, opens diffview/native diffs, then | ||
| sends the same MCP `tools/call` requests the Claude CLI sends. Prints | ||
| `REPRODUCED:`/`NOT REPRODUCED:` per phase; exits 0 when all three defects | ||
| reproduce. | ||
|
|
||
| ## Manual reproduction | ||
|
|
||
| ```bash | ||
| source fixtures/nvim-aliases.sh && vv issue-277 # cwd must be a git repo with changes | ||
| ``` | ||
|
|
||
| 1. `:DiffviewOpen` — side-by-side diff appears (2 windows with `&diff` + file panel). | ||
| 2. Connect Claude (`:ClaudeCode`, or any client with the lock-file token). | ||
| 3. Submit any prompt (or send `closeAllDiffTabs` by hand). | ||
| 4. Both diff windows close; only the Diffview panel survives. `<leader>aw` | ||
| shows the diff-window count; `:ReproState` dumps per-window state. | ||
|
|
||
| `:ReproNativeDiff <a> <b>` opens a plugin-free native vimdiff for the same | ||
| experiment (no diffview involved). | ||
|
|
||
| ## Notes | ||
|
|
||
| - diffview.nvim is cloned into `stdpath("data")/diffview.nvim` | ||
| (`~/.local/share/issue-277/`) on first start. | ||
| - The fixture exposes `v:lua.Repro277State()`, `v:lua.Repro277DiffWinCount()` | ||
| and `v:lua.Repro277Server()` for `--remote-expr` scripting. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| -- Fixture for issue #277: | ||
| -- "[BUG] closeAllDiffTabs closes all diff-mode windows, destroying unrelated | ||
| -- diffs (diffview.nvim)" | ||
| -- https://github.com/coder/claudecode.nvim/issues/277 | ||
| -- | ||
| -- Two defects under test: | ||
| -- 1. tools/close_all_diff_tabs.lua closes EVERY `&diff` window (no ownership | ||
| -- check), so diffview.nvim / fugitive / native `:diffthis` layouts are | ||
| -- destroyed when the Claude CLI fires closeAllDiffTabs (it does so at the | ||
| -- start of a user turn whenever an IDE is connected). | ||
| -- 2. find_main_editor_window (tools/open_file.lua and diff.lua) does not | ||
| -- exclude `&diff` windows, so openFile/openDiff target a diffview window | ||
| -- and :edit into it, corrupting the diff layout. | ||
| -- | ||
| -- The fixture pulls in diffview.nvim (cloned on first run) and exposes a | ||
| -- window-state probe for scripted verification: | ||
| -- nvim --server <sock> --remote-expr 'v:lua.Repro277State()' | ||
| -- | ||
| -- Usage (from repo root): | ||
| -- source fixtures/nvim-aliases.sh && vv issue-277 | ||
| -- or scripted: | ||
| -- scripts/repro_issue_277.sh | ||
| -- | ||
| -- Manual repro: open a file in a git repo with uncommitted changes, | ||
| -- :DiffviewOpen, connect claude (--ide), submit any prompt -> the side-by-side | ||
| -- diff windows close, only the Diffview file panel survives. | ||
|
|
||
| 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 = "\\" | ||
|
|
||
| -- --------------------------------------------------------------------------- | ||
| -- diffview.nvim (cloned into stdpath("data") on first run; no plugin manager) | ||
| -- --------------------------------------------------------------------------- | ||
| local diffview_dir = vim.fn.stdpath("data") .. "/diffview.nvim" | ||
| if vim.fn.isdirectory(diffview_dir) == 0 then | ||
| vim.notify("issue-277 fixture: cloning diffview.nvim ...") | ||
| local out = vim.fn.system({ | ||
| "git", | ||
| "clone", | ||
| "--depth=1", | ||
| "https://github.com/sindrets/diffview.nvim", | ||
| diffview_dir, | ||
| }) | ||
| assert(vim.v.shell_error == 0, "failed to clone diffview.nvim: " .. out) | ||
| end | ||
| vim.opt.rtp:prepend(diffview_dir) | ||
|
|
||
| local ok_dv, diffview = pcall(require, "diffview") | ||
| assert(ok_dv, "Failed to load diffview.nvim: " .. tostring(diffview)) | ||
| diffview.setup({}) | ||
|
|
||
| -- --------------------------------------------------------------------------- | ||
| -- claudecode.nvim (dev version from this repo) | ||
| -- --------------------------------------------------------------------------- | ||
| local ok, claudecode = pcall(require, "claudecode") | ||
| assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode)) | ||
|
|
||
| claudecode.setup({ | ||
| auto_start = true, -- server + lock file immediately, so scripts can connect | ||
| -- "warn", not "debug": multi-line debug echoes trip nvim's hit-enter prompt, | ||
| -- which blocks --remote-expr probes in the scripted repro. | ||
| log_level = "warn", | ||
| terminal = { | ||
| provider = "native", | ||
| auto_close = false, | ||
| }, | ||
| }) | ||
|
|
||
| vim.o.showtabline = 2 | ||
| vim.o.laststatus = 2 | ||
|
|
||
| -- --------------------------------------------------------------------------- | ||
| -- Window-state probe (for --remote-expr / on-screen verification) | ||
| -- --------------------------------------------------------------------------- | ||
|
|
||
| ---Compact state of every window across all tabpages. | ||
| ---@return string JSON: [{win,tab,name,buftype,filetype,diff}...] | ||
| function _G.Repro277State() | ||
| local out = {} | ||
| for _, win in ipairs(vim.api.nvim_list_wins()) do | ||
| local buf = vim.api.nvim_win_get_buf(win) | ||
| local name = vim.api.nvim_buf_get_name(buf) | ||
| out[#out + 1] = { | ||
| win = win, | ||
| tab = vim.api.nvim_tabpage_get_number(vim.api.nvim_win_get_tabpage(win)), | ||
| name = vim.fn.fnamemodify(name, ":t") ~= "" and vim.fn.fnamemodify(name, ":~:.") or "[No Name]", | ||
| buftype = vim.bo[buf].buftype, | ||
| filetype = vim.bo[buf].filetype, | ||
| diff = vim.wo[win].diff, | ||
| } | ||
| end | ||
| return vim.json.encode(out) | ||
| end | ||
|
|
||
| ---WebSocket endpoint of the running claudecode server ("port token", or "" if | ||
| ---not started yet). Lets scripts connect without scanning ~/.claude/ide. | ||
| ---@return string | ||
| function _G.Repro277Server() | ||
| local cc = require("claudecode") | ||
| if cc.state.port and cc.state.auth_token then | ||
| return cc.state.port .. " " .. cc.state.auth_token | ||
| end | ||
| return "" | ||
| end | ||
|
|
||
| ---Count of windows currently in diff mode (quick assertion helper). | ||
| ---@return integer | ||
| function _G.Repro277DiffWinCount() | ||
| local n = 0 | ||
| for _, win in ipairs(vim.api.nvim_list_wins()) do | ||
| if vim.wo[win].diff then | ||
| n = n + 1 | ||
| end | ||
| end | ||
| return n | ||
| end | ||
|
|
||
| vim.api.nvim_create_user_command("ReproState", function() | ||
| vim.notify(_G.Repro277State()) | ||
| end, { desc = "Show issue-277 window state" }) | ||
|
|
||
| -- Native (plugin-free) diff variant of the same bug: two `:diffsplit` windows. | ||
| vim.api.nvim_create_user_command("ReproNativeDiff", function(cmd_opts) | ||
| local args = vim.split(cmd_opts.args, "%s+") | ||
| assert(#args == 2, "usage: :ReproNativeDiff <file_a> <file_b>") | ||
| vim.cmd("edit " .. vim.fn.fnameescape(args[1])) | ||
| vim.cmd("vertical diffsplit " .. vim.fn.fnameescape(args[2])) | ||
| end, { nargs = "+", complete = "file", desc = "Open a native vimdiff of two files" }) | ||
|
|
||
| vim.keymap.set("n", "<leader>aw", function() | ||
| vim.notify(("diff windows: %d"):format(_G.Repro277DiffWinCount())) | ||
| end, { desc = "Show diff window count" }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.