Skip to content

fix(diff): close stranded tab when open_in_new_tab diff setup errors#264

Merged
ThomasK33 merged 1 commit into
mainfrom
fix/diff-new-tab-strand
Jun 8, 2026
Merged

fix(diff): close stranded tab when open_in_new_tab diff setup errors#264
ThomasK33 merged 1 commit into
mainfrom
fix/diff-new-tab-strand

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

With diff_opts.open_in_new_tab = true, M._setup_blocking_diff opens a tab early via display_terminal_in_new_tab() (which runs :tabnew). If setup then errors before M._register_diff_state runs, the post-pcall error handler could not close that tab:

  • the state-based cleanup (_cleanup_diff_state) is gated on a registered diff, which doesn't exist yet;
  • the pre-registration branch added in fix(diff): open diffs when the Claude terminal is the only window #260 only cleaned up fallback_window and the proposed buffer;
  • new_tab_handle was declared inside the pcall closure, 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 a BufReadPre/BufRead autocmd that throws (or a swap-file conflict).

Fix

Symmetric to the fallback_window/new_buffer hoists from #260:

  • Hoist new_tab_handle and original_tab_handle above the pcall.
  • In the pre-registration error branch, close the stranded tab and switch back to the original tab (mirroring how _cleanup_diff_state tears down created_new_tab).
  • Capture original_tab_handle before the display_terminal_in_new_tab() call, and fall back to the current tab when new_tab_handle is nil. This covers the deeper case (surfaced in code review) where the helper itself throws after its own :tabnew — Lua's multiple-assignment leaves new_tab_handle unassigned on an RHS throw, so relying on the return value alone would still strand the tab.

Testing

  • New regression test 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.
  • Headless repro 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 realistic BufReadPre trigger, and a control proving the harness is sound.
  • Interactive fixture 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 all green: 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

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>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@claude review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@ThomasK33

Copy link
Copy Markdown
Member Author

@claude review

@ThomasK33 ThomasK33 merged commit 74b4523 into main Jun 8, 2026
2 checks passed
@ThomasK33 ThomasK33 deleted the fix/diff-new-tab-strand branch June 8, 2026 12:55

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 review and reports mise run all green (526 tests, 0 failures).
  • Headless repro script + interactive fixture follow the established scripts/repro_issue_231.lua / fixtures/issue-238/ precedent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

diff: open_in_new_tab can strand a tab if setup errors before the diff state is registered

1 participant