From 93404f03684707025e84f55a9ecc420a044da93a Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 23 Jan 2026 22:30:30 +0100 Subject: [PATCH 1/5] fix(diff): close plugin-created split on cleanup Change-Id: Ib857da86fd15fe097651a50680d7d1639097905f Signed-off-by: Thomas Kosiewski --- CLAUDE.md | 17 ++- README.md | 13 +- lua/claudecode/diff.lua | 31 ++++- tests/unit/diff_split_window_cleanup_spec.lua | 130 ++++++++++++++++++ 4 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 tests/unit/diff_split_window_cleanup_spec.lua diff --git a/CLAUDE.md b/CLAUDE.md index 0298fc12..7a900690 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,22 +288,27 @@ require("claudecode").setup({ The `diff_opts` configuration allows you to customize diff behavior: +- `layout` ("vertical"|"horizontal", default: `"vertical"`) - Whether the diff panes open in a vertical or horizontal split. - `keep_terminal_focus` (boolean, default: `false`) - When enabled, keeps focus in the Claude Code terminal when a diff opens instead of moving focus to the diff buffer. This allows you to continue using terminal keybindings like `` for accepting/rejecting diffs without accidentally triggering other mappings. - `open_in_new_tab` (boolean, default: `false`) - Open diffs in a new tab instead of the current tab. - `hide_terminal_in_new_tab` (boolean, default: `false`) - When opening diffs in a new tab, do not show the Claude terminal split in that new tab. The terminal remains in the original tab, giving maximum screen estate for reviewing the diff. +- `on_new_file_reject` ("keep_empty"|"close_window", default: `"keep_empty"`) - Behavior when rejecting a diff for a new file (where the old file did not exist). +- Legacy aliases (still supported): `vertical_split` (maps to `layout`) and `open_in_current_tab` (inverse of `open_in_new_tab`). **Example use case**: If you frequently use `` or arrow keys in the Claude Code terminal to accept/reject diffs, enable this option to prevent focus from moving to the diff buffer where `` might trigger unintended actions. ```lua require("claudecode").setup({ diff_opts = { - keep_terminal_focus = true, -- If true, moves focus back to terminal after diff opens - open_in_new_tab = true, -- Open diff in a separate tab + layout = "vertical", -- "vertical" or "horizontal" + keep_terminal_focus = true, -- If true, moves focus back to terminal after diff opens + open_in_new_tab = true, -- Open diff in a separate tab hide_terminal_in_new_tab = true, -- In the new tab, do not show Claude terminal - auto_close_on_accept = true, - show_diff_stats = true, - vertical_split = true, - open_in_current_tab = true, + on_new_file_reject = "keep_empty", -- "keep_empty" or "close_window" + + -- Legacy aliases (still supported): + -- vertical_split = true, + -- open_in_current_tab = true, }, }) ``` diff --git a/README.md b/README.md index 117cc51d..6e85012e 100644 --- a/README.md +++ b/README.md @@ -277,10 +277,15 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md). -- Diff Integration diff_opts = { - auto_close_on_accept = true, - vertical_split = true, - open_in_current_tab = true, - keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens (including floating terminals) + layout = "vertical", -- "vertical" or "horizontal" + open_in_new_tab = false, + keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens + hide_terminal_in_new_tab = false, + -- on_new_file_reject = "keep_empty", -- "keep_empty" or "close_window" + + -- Legacy aliases (still supported): + -- vertical_split = true, + -- open_in_current_tab = true, }, }, keys = { diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index e301f8ad..79f8bb9d 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -917,6 +917,7 @@ function M._create_diff_view_from_window( existing_buffer ) local original_buffer_created_by_plugin = false + local target_window_created_by_plugin = false -- If no target window provided, create a new window in suitable location if not target_window then @@ -950,6 +951,7 @@ function M._create_diff_view_from_window( vim.api.nvim_set_current_win(target_window) create_split() original_window = vim.api.nvim_get_current_win() + target_window_created_by_plugin = true else original_window = choice.original_win end @@ -981,6 +983,7 @@ function M._create_diff_view_from_window( return { new_window = new_win, target_window = original_window, + target_window_created_by_plugin = target_window_created_by_plugin, original_buffer = original_buffer, original_buffer_created_by_plugin = original_buffer_created_by_plugin, } @@ -1050,11 +1053,19 @@ function M._cleanup_diff_state(tab_name, reason) pcall(vim.api.nvim_win_close, diff_data.new_window, true) end - -- Turn off diff mode in target window if it still exists + -- If we created an extra window/split for the diff, close it. Otherwise just disable diff mode. if diff_data.target_window and vim.api.nvim_win_is_valid(diff_data.target_window) then - vim.api.nvim_win_call(diff_data.target_window, function() - vim.cmd("diffoff") - end) + if diff_data.target_window_created_by_plugin then + -- Try a non-forced close first to avoid dropping any user edits in that window. + pcall(vim.api.nvim_win_close, diff_data.target_window, false) + end + + -- If the target window is still around, ensure diff mode is off. + if diff_data.target_window and vim.api.nvim_win_is_valid(diff_data.target_window) then + vim.api.nvim_win_call(diff_data.target_window, function() + vim.cmd("diffoff") + end) + end end -- After closing the diff in the same tab, restore terminal width if visible. @@ -1231,6 +1242,7 @@ function M._setup_blocking_diff(params, resolution_callback) new_buffer = new_buffer, new_window = diff_info.new_window, target_window = diff_info.target_window, + target_window_created_by_plugin = diff_info.target_window_created_by_plugin, original_buffer = diff_info.original_buffer, original_buffer_created_by_plugin = diff_info.original_buffer_created_by_plugin, original_cursor_pos = original_cursor_pos, @@ -1285,8 +1297,13 @@ end function M.open_diff_blocking(old_file_path, new_file_path, new_file_contents, tab_name) -- Check for existing diff with same tab_name if active_diffs[tab_name] then - -- Resolve the existing diff as rejected before replacing - M._resolve_diff_as_rejected(tab_name) + local existing_diff = active_diffs[tab_name] + -- Resolve the existing diff as rejected before replacing, but only if it was still pending. + if existing_diff.status == "pending" then + M._resolve_diff_as_rejected(tab_name) + end + -- Always clean up any leftover UI/state so we don't leak windows when reusing tab_names. + M._cleanup_diff_state(tab_name, "replaced by new diff") end -- Set up blocking diff operation @@ -1463,7 +1480,9 @@ return M ---@class DiffLayoutInfo ---@field new_window NvimWin ---@field target_window NvimWin +---@field target_window_created_by_plugin boolean ---@field original_buffer NvimBuf +---@field original_buffer_created_by_plugin boolean ---@class DiffWindowChoice ---@field decision DiffWindowDecision diff --git a/tests/unit/diff_split_window_cleanup_spec.lua b/tests/unit/diff_split_window_cleanup_spec.lua new file mode 100644 index 00000000..a534dfd5 --- /dev/null +++ b/tests/unit/diff_split_window_cleanup_spec.lua @@ -0,0 +1,130 @@ +require("tests.busted_setup") + +local function reset_vim_state_for_splits() + assert(vim and vim._mock and vim._mock.reset, "Expected vim mock with _mock.reset()") + + vim._mock.reset() + + -- Recreate a minimal tab/window state suitable for split operations. + vim._tabs = { [1] = true } + vim._current_tabpage = 1 + vim._current_window = 1000 + vim._next_winid = 1001 + + vim._mock.add_buffer(1, "/home/user/project/test.lua", "local test = {}\nreturn test", { modified = false }) + vim._mock.add_window(1000, 1, { 1, 0 }) + vim._win_tab[1000] = 1 + vim._tab_windows[1] = { 1000 } +end + +describe("Diff split window cleanup", function() + local diff + local test_old_file = "/tmp/test_split_window_cleanup_old.txt" + local tab_name = "test_split_window_cleanup_tab" + + before_each(function() + reset_vim_state_for_splits() + + -- Prepare a dummy file + local f = assert(io.open(test_old_file, "w")) + f:write("line1\nline2\n") + f:close() + + -- Minimal logger stub + package.loaded["claudecode.logger"] = { + debug = function() end, + error = function() end, + info = function() end, + warn = function() end, + } + + -- Reload diff module cleanly + package.loaded["claudecode.diff"] = nil + diff = require("claudecode.diff") + + diff.setup({ + diff_opts = { + layout = "vertical", + open_in_new_tab = false, + keep_terminal_focus = false, + }, + terminal = {}, + }) + end) + + after_each(function() + os.remove(test_old_file) + if diff and diff._cleanup_all_active_diffs then + diff._cleanup_all_active_diffs("test teardown") + end + package.loaded["claudecode.diff"] = nil + end) + + it("closes the plugin-created original split after accept when close_tab is invoked", function() + local params = { + old_file_path = test_old_file, + new_file_path = test_old_file, + new_file_contents = "new1\nnew2\n", + tab_name = tab_name, + } + + diff._setup_blocking_diff(params, function() end) + + local state = diff._get_active_diffs()[tab_name] + assert.is_table(state) + + local new_win = state.new_window + local target_win = state.target_window + + -- Should have created an extra split for the original side (target_win != 1000) + assert.are_not.equal(1000, target_win) + assert.is_true(vim.api.nvim_win_is_valid(target_win)) + assert.is_true(vim.api.nvim_win_is_valid(new_win)) + + diff._resolve_diff_as_saved(tab_name, state.new_buffer) + + -- Accept should not close windows yet + assert.is_true(vim.api.nvim_win_is_valid(target_win)) + + local closed = diff.close_diff_by_tab_name(tab_name) + assert.is_true(closed) + + assert.is_false(vim.api.nvim_win_is_valid(new_win)) + assert.is_false(vim.api.nvim_win_is_valid(target_win)) + assert.is_true(vim.api.nvim_win_is_valid(1000)) + end) + + it("does not close the reused target window when the old file is already open", function() + -- Open the old file in the main window so choose_original_window reuses it + vim.cmd("edit " .. vim.fn.fnameescape(test_old_file)) + + local params = { + old_file_path = test_old_file, + new_file_path = test_old_file, + new_file_contents = "new content\n", + tab_name = tab_name, + } + + diff._setup_blocking_diff(params, function() end) + + local state = diff._get_active_diffs()[tab_name] + assert.is_table(state) + + local new_win = state.new_window + local target_win = state.target_window + + assert.are.equal(1000, target_win) + assert.is_true(vim.api.nvim_win_is_valid(new_win)) + + diff._resolve_diff_as_saved(tab_name, state.new_buffer) + + local closed = diff.close_diff_by_tab_name(tab_name) + assert.is_true(closed) + + assert.is_false(vim.api.nvim_win_is_valid(new_win)) + assert.is_true(vim.api.nvim_win_is_valid(1000)) + + -- In reuse scenario, diff mode should have been disabled. + assert.are.equal("diffoff", vim._last_command) + end) +end) From e3baf97976c804063be05e93612e1ccd327b29b9 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Jan 2026 13:57:35 +0100 Subject: [PATCH 2/5] chore(fixtures): add repro launcher Change-Id: I89303686035f7451b457a52f85f252a2fd2ddd5c Signed-off-by: Thomas Kosiewski --- DEVELOPMENT.md | 3 ++ fixtures/bin/repro | 48 ++++++++++++++++++++++++++ fixtures/nvim-aliases.sh | 3 ++ fixtures/repro/example/README.md | 58 ++++++++++++++++++++++++++++++++ fixtures/repro/example/a.txt | 4 +++ fixtures/repro/example/b.txt | 2 ++ fixtures/repro/init.lua | 47 ++++++++++++++++++++++++++ 7 files changed, 165 insertions(+) create mode 100755 fixtures/bin/repro create mode 100644 fixtures/repro/example/README.md create mode 100644 fixtures/repro/example/a.txt create mode 100644 fixtures/repro/example/b.txt create mode 100644 fixtures/repro/init.lua diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7c0311ad..ca704373 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -153,6 +153,9 @@ vv netrw # Start Neovim with built-in netrw configuration # List available configurations list-configs + +# Minimal repro environment (copies fixtures/repro/example into /tmp) +repro ``` **Example fixture structure** (`fixtures/my-integration/`): diff --git a/fixtures/bin/repro b/fixtures/bin/repro new file mode 100755 index 00000000..88199d85 --- /dev/null +++ b/fixtures/bin/repro @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euo pipefail + +# repro - Launch Neovim with the minimal "repro" fixture + a temp workspace. +# +# This is intended to provide a low-noise environment for reproducing issues in claudecode.nvim. + +FIXTURES_DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)" + +# Source common functions +source "$FIXTURES_DIR/bin/common.sh" + +config="repro" +if ! validate_config "$FIXTURES_DIR" "$config"; then + exit 1 +fi + +template_dir="$FIXTURES_DIR/$config/example" +if [[ ! -d "$template_dir" ]]; then + echo "Error: repro template directory not found: $template_dir" >&2 + exit 1 +fi + +workspace_dir="${TMPDIR:-/tmp}/claudecode.nvim-repro" + +# Safety check before deleting +if [[ -z "$workspace_dir" || "$workspace_dir" == "/" ]]; then + echo "Error: unsafe workspace_dir: '$workspace_dir'" >&2 + exit 1 +fi + +rm -rf "$workspace_dir" +mkdir -p "$workspace_dir" +cp -R "$template_dir/." "$workspace_dir/" + +echo "Repro workspace: $workspace_dir" +echo "Repro config: $FIXTURES_DIR/$config" +echo "Tip: run 'vve repro' to edit the config or open $template_dir/README.md for steps." + +# If no args are provided, open the default file to keep the window non-empty. +if [[ $# -eq 0 ]]; then + nvim_args=("a.txt") +else + nvim_args=("$@") +fi + +(cd "$workspace_dir" && NVIM_APPNAME="$config" XDG_CONFIG_HOME="$FIXTURES_DIR" nvim "${nvim_args[@]}") diff --git a/fixtures/nvim-aliases.sh b/fixtures/nvim-aliases.sh index d2748938..5cf0d8d7 100755 --- a/fixtures/nvim-aliases.sh +++ b/fixtures/nvim-aliases.sh @@ -14,7 +14,10 @@ alias vv="$BIN_DIR/vv" alias vve="$BIN_DIR/vve" # shellcheck disable=SC2139 alias list-configs="$BIN_DIR/list-configs" +# shellcheck disable=SC2139 +alias repro="$BIN_DIR/repro" echo "Neovim configuration aliases loaded!" echo "Use 'vv ' or 'vve ' to test configurations" +echo "Use 'repro' for a minimal claudecode.nvim repro environment" echo "Use 'list-configs' to see available options" diff --git a/fixtures/repro/example/README.md b/fixtures/repro/example/README.md new file mode 100644 index 00000000..c1b5e065 --- /dev/null +++ b/fixtures/repro/example/README.md @@ -0,0 +1,58 @@ +# claudecode.nvim repro workspace + +This directory is copied into a temp workspace when you run `repro`. + +## Quick start + +From the repo root: + +```sh +source fixtures/nvim-aliases.sh +repro +``` + +That will: + +- create `/tmp/claudecode.nvim-repro` (reset on every run) +- open Neovim with the **minimal** `fixtures/repro` config +- open `a.txt` so your current window is non-empty + +## Reproducing issue #155 (leftover diff split) + +Goal: confirm that after accepting a diff for a file that was *not* already open, we do **not** leave behind an extra split. + +1. In Neovim, note window count: + + ```vim + :echo winnr('$') + ``` + +2. Start Claude: + + ```vim + :ClaudeCode + ``` + +3. In the Claude terminal, ask Claude to make a small edit to `b.txt`. + + **Important:** Do *not* open `b.txt` in a Neovim window yourself before the diff opens. + +4. When the diff opens, accept it: + + - `:w` from the proposed buffer **or** + - `aa` + +5. Wait for Claude to close the diff (the plugin cleans up when Claude calls `close_tab`). + +6. Confirm window count returned to what it was in step 1: + + ```vim + :echo winnr('$') + ``` + +You should not see an orphaned split after accept. + +## Notes + +- This fixture uses the **native** terminal provider to avoid depending on external plugins. +- To tweak the config, edit `fixtures/repro/init.lua` (or run `vve repro`). diff --git a/fixtures/repro/example/a.txt b/fixtures/repro/example/a.txt new file mode 100644 index 00000000..1128bd19 --- /dev/null +++ b/fixtures/repro/example/a.txt @@ -0,0 +1,4 @@ +This is file A. + +Keep this file open. +Ask Claude to edit b.txt (without opening b.txt in a window first). diff --git a/fixtures/repro/example/b.txt b/fixtures/repro/example/b.txt new file mode 100644 index 00000000..c0d0fb45 --- /dev/null +++ b/fixtures/repro/example/b.txt @@ -0,0 +1,2 @@ +line1 +line2 diff --git a/fixtures/repro/init.lua b/fixtures/repro/init.lua new file mode 100644 index 00000000..d98910a6 --- /dev/null +++ b/fixtures/repro/init.lua @@ -0,0 +1,47 @@ +-- Minimal repro config for claudecode.nvim issues. +-- +-- This fixture intentionally avoids a plugin manager so it's easy to run and reason about. +-- +-- Usage (from repo root): +-- source fixtures/nvim-aliases.sh +-- repro +-- +-- To edit this config: +-- vve repro + +-- Ensure this repo is on the runtimepath so `plugin/claudecode.lua` is loaded. +local config_dir = vim.fn.stdpath("config") +local repo_root = vim.fn.fnamemodify(config_dir, ":h:h") + +vim.opt.rtp:prepend(repo_root) + +-- Basic editor settings +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +local ok, claudecode = pcall(require, "claudecode") +assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode)) + +claudecode.setup({ + log_level = "debug", + terminal = { + provider = "native", + auto_close = false, + }, + diff_opts = { + layout = "vertical", + open_in_new_tab = false, + keep_terminal_focus = false, + }, +}) + +-- Keymaps (kept small on purpose) +vim.keymap.set("n", "ac", "ClaudeCode", { desc = "Toggle Claude" }) +vim.keymap.set("n", "af", "ClaudeCodeFocus", { desc = "Focus Claude" }) + +vim.keymap.set("n", "aa", "ClaudeCodeDiffAccept", { desc = "Accept diff" }) +vim.keymap.set("n", "ad", "ClaudeCodeDiffDeny", { desc = "Deny diff" }) + +vim.keymap.set("n", "aw", function() + vim.notify(("windows in tab: %d"):format(vim.fn.winnr("$"))) +end, { desc = "Claude: show window count" }) From fc643c4a20dd48231541e651f6f6155af1737ce3 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Jan 2026 14:17:35 +0100 Subject: [PATCH 3/5] docs(fixtures): make repro fixture generic Change-Id: I156670025cd239eebf824e7c9ed3fc67d90d6625 Signed-off-by: Thomas Kosiewski --- fixtures/bin/repro | 57 +++++++++++++++++++++++++++----- fixtures/repro/example/README.md | 44 ++++++++++++------------ fixtures/repro/init.lua | 46 ++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/fixtures/bin/repro b/fixtures/bin/repro index 88199d85..f9686d2a 100755 --- a/fixtures/bin/repro +++ b/fixtures/bin/repro @@ -16,6 +16,45 @@ if ! validate_config "$FIXTURES_DIR" "$config"; then exit 1 fi +keep_workspace=false + +# Parse repro-specific flags. +# Everything else is passed to nvim. +while [[ $# -gt 0 ]]; do + case "$1" in + --keep|--no-reset) + keep_workspace=true + shift + ;; + --help|-h) + cat <<'EOF' +repro - Launch Neovim with the minimal "repro" fixture + a temp workspace. + +Usage: + repro [--keep] [nvim args...] + +Options: + --keep, --no-reset Reuse the existing repro workspace instead of resetting it + +Examples: + repro + repro --keep + repro --headless "+qall!" +EOF + exit 0 + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done + +nvim_args=("$@") + template_dir="$FIXTURES_DIR/$config/example" if [[ ! -d "$template_dir" ]]; then echo "Error: repro template directory not found: $template_dir" >&2 @@ -30,19 +69,21 @@ if [[ -z "$workspace_dir" || "$workspace_dir" == "/" ]]; then exit 1 fi -rm -rf "$workspace_dir" -mkdir -p "$workspace_dir" -cp -R "$template_dir/." "$workspace_dir/" +if [[ "$keep_workspace" == "true" && -d "$workspace_dir" ]]; then + echo "Repro workspace (kept): $workspace_dir" +else + rm -rf "$workspace_dir" + mkdir -p "$workspace_dir" + cp -R "$template_dir/." "$workspace_dir/" + echo "Repro workspace (reset): $workspace_dir" +fi -echo "Repro workspace: $workspace_dir" echo "Repro config: $FIXTURES_DIR/$config" -echo "Tip: run 'vve repro' to edit the config or open $template_dir/README.md for steps." +echo "Tip: :e README.md in the repro workspace for steps." # If no args are provided, open the default file to keep the window non-empty. -if [[ $# -eq 0 ]]; then +if [[ ${#nvim_args[@]} -eq 0 ]]; then nvim_args=("a.txt") -else - nvim_args=("$@") fi (cd "$workspace_dir" && NVIM_APPNAME="$config" XDG_CONFIG_HOME="$FIXTURES_DIR" nvim "${nvim_args[@]}") diff --git a/fixtures/repro/example/README.md b/fixtures/repro/example/README.md index c1b5e065..59980099 100644 --- a/fixtures/repro/example/README.md +++ b/fixtures/repro/example/README.md @@ -13,44 +13,42 @@ repro That will: -- create `/tmp/claudecode.nvim-repro` (reset on every run) +- create `/tmp/claudecode.nvim-repro` (reset on every run; use `repro --keep` to reuse) - open Neovim with the **minimal** `fixtures/repro` config - open `a.txt` so your current window is non-empty -## Reproducing issue #155 (leftover diff split) +## Iterating on the config -Goal: confirm that after accepting a diff for a file that was *not* already open, we do **not** leave behind an extra split. +The Neovim config lives at `fixtures/repro/init.lua`. -1. In Neovim, note window count: +- Edit it from another terminal: - ```vim - :echo winnr('$') - ``` + ```sh + vve repro + ``` -2. Start Claude: + Then restart the running `repro` Neovim instance to pick up changes. - ```vim - :ClaudeCode - ``` +- Or edit it from inside the running `repro` session: -3. In the Claude terminal, ask Claude to make a small edit to `b.txt`. + ```vim + :ReproEditConfig + ``` - **Important:** Do *not* open `b.txt` in a Neovim window yourself before the diff opens. +> Note: config changes generally require restarting Neovim (this fixture avoids a plugin manager / hot-reload). -4. When the diff opens, accept it: +## Example flow (sanity check) - - `:w` from the proposed buffer **or** - - `aa` +A basic end-to-end diff flow you can use to sanity-check the environment: -5. Wait for Claude to close the diff (the plugin cleans up when Claude calls `close_tab`). +1. Start Claude: -6. Confirm window count returned to what it was in step 1: + - press `ac` (starts the server if needed, then opens the terminal), **or** + - run `:ClaudeCodeStart` then `:ClaudeCode` - ```vim - :echo winnr('$') - ``` - -You should not see an orphaned split after accept. +2. Ask Claude to edit `b.txt` (do not open it in a window first) +3. Accept the diff with `:w` (or `aa`) +4. Confirm you didn’t get any extra leftover windows: `:echo winnr('$')` ## Notes diff --git a/fixtures/repro/init.lua b/fixtures/repro/init.lua index d98910a6..f11515be 100644 --- a/fixtures/repro/init.lua +++ b/fixtures/repro/init.lua @@ -23,6 +23,7 @@ local ok, claudecode = pcall(require, "claudecode") assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode)) claudecode.setup({ + auto_start = false, -- avoid noisy startup + make restarts deterministic log_level = "debug", terminal = { provider = "native", @@ -35,13 +36,54 @@ claudecode.setup({ }, }) +local function ensure_claudecode_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 + + local started = started_or_err + if started then + return true + end + + -- start() returns false + "Already running" when running. + if 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 + -- Keymaps (kept small on purpose) -vim.keymap.set("n", "ac", "ClaudeCode", { desc = "Toggle Claude" }) -vim.keymap.set("n", "af", "ClaudeCodeFocus", { desc = "Focus Claude" }) +vim.keymap.set("n", "ac", function() + if ensure_claudecode_started() then + vim.cmd("ClaudeCode") + end +end, { desc = "Toggle Claude" }) + +vim.keymap.set("n", "af", function() + if ensure_claudecode_started() then + vim.cmd("ClaudeCodeFocus") + end +end, { desc = "Focus Claude" }) vim.keymap.set("n", "aa", "ClaudeCodeDiffAccept", { desc = "Accept diff" }) vim.keymap.set("n", "ad", "ClaudeCodeDiffDeny", { desc = "Deny diff" }) + +-- Convenience helpers for iterating on this fixture. +vim.api.nvim_create_user_command("ReproEditConfig", function() + local config_path = vim.fn.stdpath("config") .. "/init.lua" + vim.cmd("edit " .. vim.fn.fnameescape(config_path)) +end, { desc = "Edit the repro Neovim config" }) + +vim.keymap.set("n", "ae", "ReproEditConfig", { desc = "Edit repro config" }) vim.keymap.set("n", "aw", function() vim.notify(("windows in tab: %d"):format(vim.fn.winnr("$"))) end, { desc = "Claude: show window count" }) From 2078f501d0be5162c95e616af0df5799fb62b396 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Jan 2026 14:31:00 +0100 Subject: [PATCH 4/5] fix(fixtures): avoid treesitter injection errors in repro config Change-Id: I812c397919e884b243e275c1c1a519254d57893e Signed-off-by: Thomas Kosiewski --- fixtures/repro/init.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fixtures/repro/init.lua b/fixtures/repro/init.lua index f11515be..e97ed684 100644 --- a/fixtures/repro/init.lua +++ b/fixtures/repro/init.lua @@ -63,13 +63,15 @@ end -- Keymaps (kept small on purpose) vim.keymap.set("n", "ac", function() if ensure_claudecode_started() then - vim.cmd("ClaudeCode") + local terminal = require("claudecode.terminal") + terminal.simple_toggle({}, nil) end end, { desc = "Toggle Claude" }) vim.keymap.set("n", "af", function() if ensure_claudecode_started() then - vim.cmd("ClaudeCodeFocus") + local terminal = require("claudecode.terminal") + terminal.focus_toggle({}, nil) end end, { desc = "Focus Claude" }) @@ -80,7 +82,12 @@ vim.keymap.set("n", "ad", "ClaudeCodeDiffDeny", { desc = "Deny -- Convenience helpers for iterating on this fixture. vim.api.nvim_create_user_command("ReproEditConfig", function() local config_path = vim.fn.stdpath("config") .. "/init.lua" - vim.cmd("edit " .. vim.fn.fnameescape(config_path)) + + -- Open the config file without `:edit` / `vim.cmd(...)` so we don't trigger + -- Treesitter "vim" language injections (which can be noisy if parsers/queries mismatch). + local bufnr = vim.fn.bufadd(config_path) + vim.fn.bufload(bufnr) + vim.api.nvim_set_current_buf(bufnr) end, { desc = "Edit the repro Neovim config" }) vim.keymap.set("n", "ae", "ReproEditConfig", { desc = "Edit repro config" }) From 037efb7d0e72ac4b95e4db43b1285a9e1568e9b6 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Jan 2026 15:03:10 +0100 Subject: [PATCH 5/5] chore(fixtures): format repro init.lua Change-Id: I8e57f75c6884c4a669cb82928400820a22d8b75a Signed-off-by: Thomas Kosiewski --- fixtures/repro/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/fixtures/repro/init.lua b/fixtures/repro/init.lua index e97ed684..58a9b668 100644 --- a/fixtures/repro/init.lua +++ b/fixtures/repro/init.lua @@ -78,7 +78,6 @@ end, { desc = "Focus Claude" }) vim.keymap.set("n", "aa", "ClaudeCodeDiffAccept", { desc = "Accept diff" }) vim.keymap.set("n", "ad", "ClaudeCodeDiffDeny", { desc = "Deny diff" }) - -- Convenience helpers for iterating on this fixture. vim.api.nvim_create_user_command("ReproEditConfig", function() local config_path = vim.fn.stdpath("config") .. "/init.lua"