fix(diff): close stranded tab when open_in_new_tab diff setup errors#264
Conversation
With diff_opts.open_in_new_tab, _setup_blocking_diff opens a tab early via display_terminal_in_new_tab() (it runs :tabnew). If setup then errored before the diff state was registered, neither cleanup path closed it: the state-based cleanup is gated on a registered diff, and the pre-registration branch (added in #260) only handled the fallback window and proposed buffer. new_tab_handle was scoped inside the pcall closure, so the error handler couldn't reach it, leaving one stranded tab per failed setup with the original tab unfocused. Hoist new_tab_handle/original_tab_handle above the pcall (mirroring the #260 fallback_window/new_buffer hoists) and close the tab + refocus in the pre-registration branch. original_tab_handle is captured before the display_terminal_in_new_tab() call, with a current-tab fallback, so a throw inside that helper after its :tabnew (Lua leaves the multiple-assignment target unassigned) is covered too. Add a regression test plus a headless repro and fixture (matching the scripts/repro_issue_231 and fixtures/issue-238 precedent). Closes #262 Change-Id: I366f657a1858c0d7a20c6b9a8660c0a580b5bdb7 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@claude review |
There was a problem hiding this comment.
LGTM — symmetric to #260, with tests covering both the post-helper error and the throw-inside-helper case.
Extended reasoning...
Overview
This PR fixes a tab leak in M._setup_blocking_diff (lua/claudecode/diff.lua) when diff_opts.open_in_new_tab = true. The fix hoists new_tab_handle and original_tab_handle above the pcall so the pre-registration error branch can close the tab opened by display_terminal_in_new_tab() and refocus the original. Also adds a regression test, a headless repro script, and an interactive fixture for issue #262.
Security risks
None. The change is purely UI/window-management cleanup — no auth, IO, crypto, or external input paths are touched. The new tab close is wrapped in pcall and guarded by vim.api.nvim_tabpage_is_valid checks.
Level of scrutiny
Low. This is a 5-line behavioral change (plus comments) in already-well-tested cleanup code, symmetric to the fallback_window/new_buffer hoists added in #260 in the same function. The fallback branch (using current tab when new_tab_handle is nil) defensively handles Lua's multiple-assignment semantics when the helper throws mid-way, and the stranded_tab ~= original_tab_handle guard prevents accidentally closing the user's tab.
Other factors
- Three regression tests cover the happy path (precondition), the deterministic error-after-tabnew case, and the harder throw-inside-helper case.
- Codex reviewed and found no issues.
- Bug hunting system found no issues.
- Author explicitly invited
@claude reviewand reportsmise run allgreen (526 tests, 0 failures). - Headless repro script + interactive fixture follow the established
scripts/repro_issue_231.lua/fixtures/issue-238/precedent.
Summary
With
diff_opts.open_in_new_tab = true,M._setup_blocking_diffopens a tab early viadisplay_terminal_in_new_tab()(which runs:tabnew). If setup then errors beforeM._register_diff_stateruns, the post-pcallerror handler could not close that tab:_cleanup_diff_state) is gated on a registered diff, which doesn't exist yet;fallback_windowand the proposed buffer;new_tab_handlewas declared inside thepcallclosure, so the error handler couldn't reach it.Result: one stranded empty tab per failed setup, and the original tab was not refocused. The most realistic trigger is
load_original_buffer's:edit <old_file>hitting aBufReadPre/BufReadautocmd that throws (or a swap-file conflict).Fix
Symmetric to the
fallback_window/new_bufferhoists from #260:new_tab_handleandoriginal_tab_handleabove thepcall._cleanup_diff_statetears downcreated_new_tab).original_tab_handlebefore thedisplay_terminal_in_new_tab()call, and fall back to the current tab whennew_tab_handleisnil. This covers the deeper case (surfaced in code review) where the helper itself throws after its own:tabnew— Lua's multiple-assignment leavesnew_tab_handleunassigned on an RHS throw, so relying on the return value alone would still strand the tab.Testing
tests/unit/diff_new_tab_strand_spec.lua(fails on the old code, passes now): asserts no tab survives an error before registration, that focus returns to the original tab, and covers both the post-helper error and the throw-inside-helper case.scripts/repro_issue_262.lua(nvim --headless -u NONE -l scripts/repro_issue_262.lua; exit 1 on bug, 0 when fixed) with a deterministic scenario, the realisticBufReadPretrigger, and a control proving the harness is sound.fixtures/issue-262/(vv issue-262, then:ReproStrandTab) — verified via a real Neovim TUI that the tabline no longer grows on failure and a successful new-tab diff still opens and persists.mise run allgreen: 526 tests, 0 failures, luacheck clean, formatted.The repro script + fixture follow the existing
scripts/repro_issue_231.lua/fixtures/issue-238/precedent.Closes #262
🤖 Generated with Claude Code