Skip to content

Commit 0e864f3

Browse files
committed
feat: add keep_terminal_focus option for diff views
Change-Id: I196b3e69832c55aa56ab07b185cd643b3937b8a8 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent d0f9748 commit 0e864f3

File tree

9 files changed

+162
-10
lines changed

9 files changed

+162
-10
lines changed

CLAUDE.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,32 @@ Enable detailed authentication logging by setting:
252252

253253
```lua
254254
require("claudecode").setup({
255-
log_level = "debug" -- Shows auth token generation, validation, and failures
255+
log_level = "debug", -- Shows auth token generation, validation, and failures
256+
diff_opts = {
257+
keep_terminal_focus = true, -- If true, moves focus back to terminal after diff opens
258+
},
259+
})
260+
```
261+
262+
### Configuration Options
263+
264+
#### Diff Options
265+
266+
The `diff_opts` configuration allows you to customize diff behavior:
267+
268+
- `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 `<CR>` for accepting/rejecting diffs without accidentally triggering other mappings.
269+
270+
**Example use case**: If you frequently use `<CR>` 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 `<CR>` might trigger unintended actions.
271+
272+
```lua
273+
require("claudecode").setup({
274+
diff_opts = {
275+
keep_terminal_focus = true, -- If true, moves focus back to terminal after diff opens
276+
auto_close_on_accept = true,
277+
show_diff_stats = true,
278+
vertical_split = true,
279+
open_in_current_tab = true,
280+
},
256281
})
257282
```
258283

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
267267
auto_close_on_accept = true,
268268
vertical_split = true,
269269
open_in_current_tab = true,
270+
keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens
270271
},
271272
},
272273
keys = {

dev-config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ return {
6262
-- show_diff_stats = true, -- Show diff statistics
6363
-- vertical_split = true, -- Use vertical split for diffs
6464
-- open_in_current_tab = true, -- Open diffs in current tab vs new tab
65+
-- keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens
6566
-- },
6667

6768
-- Terminal Configuration

lua/claudecode/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ M.defaults = {
1818
show_diff_stats = true,
1919
vertical_split = true,
2020
open_in_current_tab = true, -- Use current tab instead of creating new tab
21+
keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens
2122
},
2223
models = {
2324
{ name = "Claude Opus 4 (Latest)", value = "opus" },
@@ -78,6 +79,7 @@ function M.validate(config)
7879
assert(type(config.diff_opts.show_diff_stats) == "boolean", "diff_opts.show_diff_stats must be a boolean")
7980
assert(type(config.diff_opts.vertical_split) == "boolean", "diff_opts.vertical_split must be a boolean")
8081
assert(type(config.diff_opts.open_in_current_tab) == "boolean", "diff_opts.open_in_current_tab must be a boolean")
82+
assert(type(config.diff_opts.keep_terminal_focus) == "boolean", "diff_opts.keep_terminal_focus must be a boolean")
8183

8284
-- Validate env
8385
assert(type(config.env) == "table", "env must be a table")

lua/claudecode/diff.lua

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local logger = require("claudecode.logger")
77
-- Global state management for active diffs
88
local active_diffs = {}
99
local autocmd_group
10+
local config
1011

1112
--- Get or create the autocmd group
1213
local function get_autocmd_group()
@@ -41,13 +42,12 @@ local function find_main_editor_window()
4142
is_suitable = false
4243
end
4344

44-
-- Skip known sidebar filetypes and ClaudeCode terminal
45+
-- Skip known sidebar filetypes
4546
if
4647
is_suitable
4748
and (
4849
filetype == "neo-tree"
4950
or filetype == "neo-tree-popup"
50-
or filetype == "ClaudeCode"
5151
or filetype == "NvimTree"
5252
or filetype == "oil"
5353
or filetype == "aerial"
@@ -66,11 +66,39 @@ local function find_main_editor_window()
6666
return nil
6767
end
6868

69+
--- Find the Claude Code terminal window to keep focus there.
70+
-- Uses the terminal provider to get the active terminal buffer, then finds its window.
71+
-- @return number|nil Window ID of the Claude Code terminal window, or nil if not found
72+
local function find_claudecode_terminal_window()
73+
local terminal_ok, terminal_module = pcall(require, "claudecode.terminal")
74+
if not terminal_ok then
75+
return nil
76+
end
77+
78+
local terminal_bufnr = terminal_module.get_active_terminal_bufnr()
79+
if not terminal_bufnr then
80+
return nil
81+
end
82+
83+
-- Find the window containing this buffer
84+
for _, win in ipairs(vim.api.nvim_list_wins()) do
85+
if vim.api.nvim_win_get_buf(win) == terminal_bufnr then
86+
local win_config = vim.api.nvim_win_get_config(win)
87+
-- Skip floating windows
88+
if not (win_config.relative and win_config.relative ~= "") then
89+
return win
90+
end
91+
end
92+
end
93+
94+
return nil
95+
end
96+
6997
--- Setup the diff module
70-
-- @param user_diff_config table|nil Reserved for future use
71-
function M.setup(user_diff_config)
72-
-- Currently no configuration needed for native diff
73-
-- Parameter kept for API compatibility
98+
-- @param user_config table|nil The configuration passed from init.lua
99+
function M.setup(user_config)
100+
-- Store the configuration for later use
101+
config = user_config or {}
74102
end
75103

76104
--- Open a diff view between two files
@@ -513,7 +541,7 @@ function M._create_diff_view_from_window(target_window, old_file_path, new_buffe
513541
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
514542
local filetype = vim.api.nvim_buf_get_option(buf, "filetype")
515543

516-
if buftype == "terminal" or buftype == "prompt" or filetype == "neo-tree" or filetype == "ClaudeCode" then
544+
if buftype == "terminal" or buftype == "prompt" or filetype == "neo-tree" then
517545
vim.cmd("vsplit")
518546
end
519547

@@ -576,13 +604,23 @@ function M._create_diff_view_from_window(target_window, old_file_path, new_buffe
576604
vim.cmd("diffthis")
577605

578606
vim.cmd("wincmd =")
607+
608+
-- Always focus the diff window first for proper visual flow and window arrangement
579609
vim.api.nvim_set_current_win(new_win)
580610

581611
-- Store diff context in buffer variables for user commands
582612
vim.b[new_buffer].claudecode_diff_tab_name = tab_name
583613
vim.b[new_buffer].claudecode_diff_new_win = new_win
584614
vim.b[new_buffer].claudecode_diff_target_win = target_window
585615

616+
-- After all diff setup is complete, optionally return focus to terminal
617+
if config and config.diff_opts and config.diff_opts.keep_terminal_focus then
618+
local terminal_win = find_claudecode_terminal_window()
619+
if terminal_win then
620+
vim.api.nvim_set_current_win(terminal_win)
621+
end
622+
end
623+
586624
-- Return window information for later storage
587625
return {
588626
new_window = new_win,

lua/claudecode/init.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ M.version = {
4343
--- @field connection_wait_delay number Milliseconds to wait after connection before sending queued @ mentions.
4444
--- @field connection_timeout number Maximum time to wait for Claude Code to connect (milliseconds).
4545
--- @field queue_timeout number Maximum time to keep @ mentions in queue (milliseconds).
46-
--- @field diff_opts { auto_close_on_accept: boolean, show_diff_stats: boolean, vertical_split: boolean, open_in_current_tab: boolean } Options for the diff provider.
46+
--- @field diff_opts { auto_close_on_accept: boolean, show_diff_stats: boolean, vertical_split: boolean, open_in_current_tab: boolean, keep_terminal_focus: boolean } Options for the diff provider.
4747

4848
--- @type ClaudeCode.Config
4949
local default_config = {
@@ -62,6 +62,7 @@ local default_config = {
6262
show_diff_stats = true,
6363
vertical_split = true,
6464
open_in_current_tab = false,
65+
keep_terminal_focus = false,
6566
},
6667
}
6768

lua/claudecode/tools/open_file.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ local function find_main_editor_window()
8888
and (
8989
filetype == "neo-tree"
9090
or filetype == "neo-tree-popup"
91-
or filetype == "ClaudeCode"
9291
or filetype == "NvimTree"
9392
or filetype == "oil"
9493
or filetype == "aerial"

tests/unit/config_spec.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ describe("Configuration", function()
2323
expect(config.defaults).to_have_key("log_level")
2424
expect(config.defaults).to_have_key("track_selection")
2525
expect(config.defaults).to_have_key("models")
26+
expect(config.defaults).to_have_key("diff_opts")
27+
expect(config.defaults.diff_opts).to_have_key("keep_terminal_focus")
28+
expect(config.defaults.diff_opts.keep_terminal_focus).to_be_false()
2629
end)
2730

2831
it("should apply and validate user configuration", function()
@@ -136,5 +139,62 @@ describe("Configuration", function()
136139
expect(merged_config.models).to_be_table()
137140
end)
138141

142+
it("should accept valid keep_terminal_focus configuration", function()
143+
local user_config = {
144+
port_range = { min = 10000, max = 65535 },
145+
auto_start = true,
146+
log_level = "info",
147+
track_selection = true,
148+
visual_demotion_delay_ms = 50,
149+
connection_wait_delay = 200,
150+
connection_timeout = 10000,
151+
queue_timeout = 5000,
152+
diff_opts = {
153+
auto_close_on_accept = true,
154+
show_diff_stats = true,
155+
vertical_split = true,
156+
open_in_current_tab = true,
157+
keep_terminal_focus = true,
158+
},
159+
env = {},
160+
models = {
161+
{ name = "Test Model", value = "test" },
162+
},
163+
}
164+
165+
local final_config = config.apply(user_config)
166+
expect(final_config.diff_opts.keep_terminal_focus).to_be_true()
167+
end)
168+
169+
it("should reject invalid keep_terminal_focus configuration", function()
170+
local invalid_config = {
171+
port_range = { min = 10000, max = 65535 },
172+
auto_start = true,
173+
log_level = "info",
174+
track_selection = true,
175+
visual_demotion_delay_ms = 50,
176+
connection_wait_delay = 200,
177+
connection_timeout = 10000,
178+
queue_timeout = 5000,
179+
diff_opts = {
180+
auto_close_on_accept = true,
181+
show_diff_stats = true,
182+
vertical_split = true,
183+
open_in_current_tab = true,
184+
keep_terminal_focus = "invalid", -- Should be boolean
185+
},
186+
env = {},
187+
models = {
188+
{ name = "Test Model", value = "test" },
189+
},
190+
}
191+
192+
local success, _ = pcall(function()
193+
config.validate(invalid_config)
194+
end)
195+
196+
expect(success).to_be_false()
197+
end)
198+
139199
teardown()
140200
end)

tests/unit/diff_spec.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ describe("Diff Module", function()
4949
teardown()
5050
end)
5151

52+
describe("Configuration", function()
53+
it("should store configuration in setup", function()
54+
local test_config = {
55+
diff_opts = {
56+
keep_terminal_focus = true,
57+
},
58+
}
59+
60+
diff.setup(test_config)
61+
62+
-- We can't directly test the stored config since it's local to the module,
63+
-- but we can test that setup doesn't error and the module is properly initialized
64+
expect(type(diff.setup)).to_be("function")
65+
expect(type(diff.open_diff)).to_be("function")
66+
end)
67+
68+
it("should handle empty configuration", function()
69+
-- This should not error
70+
diff.setup(nil)
71+
diff.setup({})
72+
73+
expect(type(diff.setup)).to_be("function")
74+
end)
75+
end)
76+
5277
describe("Temporary File Management (via Native Diff)", function()
5378
it("should create temporary files with correct content through native diff", function()
5479
local test_content = "This is test content\nLine 2\nLine 3"

0 commit comments

Comments
 (0)