Skip to content

Commit 1020fb5

Browse files
xieanfengclaude
andcommitted
feat(terminal): add auto_insert option to preserve scroll position
When switching back to the Claude Code terminal from the editor, the terminal auto-enters insert mode and scrolls to the bottom, losing the user's reading position. Add `terminal.auto_insert` config option (default: true) that when set to false, keeps the terminal in normal mode and preserves the scroll position. Closes #232 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 432121f commit 1020fb5

5 files changed

Lines changed: 90 additions & 37 deletions

File tree

lua/claudecode/diff.lua

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,20 @@ local function display_terminal_in_new_tab()
278278
apply_window_options(terminal_win, terminal_options)
279279

280280
-- Set up autocmd to enter terminal mode when focusing this terminal window
281-
vim.api.nvim_create_autocmd("BufEnter", {
282-
buffer = terminal_bufnr,
283-
group = get_autocmd_group(),
284-
callback = function()
285-
-- Only enter insert mode if we're in a terminal buffer and in normal mode
286-
if vim.bo.buftype == "terminal" and vim.fn.mode() == "n" then
287-
vim.cmd("startinsert")
288-
end
289-
end,
290-
desc = "Auto-enter terminal mode when focusing Claude Code terminal",
291-
})
281+
local terminal_auto_insert = not config or not config.terminal or config.terminal.auto_insert ~= false
282+
if terminal_auto_insert then
283+
vim.api.nvim_create_autocmd("BufEnter", {
284+
buffer = terminal_bufnr,
285+
group = get_autocmd_group(),
286+
callback = function()
287+
-- Only enter insert mode if we're in a terminal buffer and in normal mode
288+
if vim.bo.buftype == "terminal" and vim.fn.mode() == "n" then
289+
vim.cmd("startinsert")
290+
end
291+
end,
292+
desc = "Auto-enter terminal mode when focusing Claude Code terminal",
293+
})
294+
end
292295

293296
local total_width = vim.o.columns
294297
local terminal_width = math.floor(total_width * split_width)
@@ -596,17 +599,22 @@ local function setup_new_buffer(
596599
vim.b[new_buf].claudecode_diff_target_win = target_win_for_meta
597600

598601
if config and config.diff_opts and config.diff_opts.keep_terminal_focus then
602+
local auto_insert = not config.terminal or config.terminal.auto_insert ~= false
599603
vim.schedule(function()
600604
if terminal_win_in_new_tab and vim.api.nvim_win_is_valid(terminal_win_in_new_tab) then
601605
vim.api.nvim_set_current_win(terminal_win_in_new_tab)
602-
vim.cmd("startinsert")
606+
if auto_insert then
607+
vim.cmd("startinsert")
608+
end
603609
return
604610
end
605611

606612
local terminal_win = find_claudecode_terminal_window()
607613
if terminal_win then
608614
vim.api.nvim_set_current_win(terminal_win)
609-
vim.cmd("startinsert")
615+
if auto_insert then
616+
vim.cmd("startinsert")
617+
end
610618
end
611619
end)
612620
end

lua/claudecode/terminal.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ local defaults = {
1717
external_terminal_cmd = nil,
1818
},
1919
auto_close = true,
20+
auto_insert = true,
2021
env = {},
2122
snacks_win_opts = {},
2223
-- Working directory control
@@ -268,6 +269,7 @@ local function build_config(opts_override)
268269
split_side = effective_config.split_side,
269270
split_width_percentage = effective_config.split_width_percentage,
270271
auto_close = effective_config.auto_close,
272+
auto_insert = effective_config.auto_insert,
271273
snacks_win_opts = effective_config.snacks_win_opts,
272274
cwd = resolved_cwd,
273275
}
@@ -447,6 +449,12 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
447449
else
448450
vim.notify("claudecode.terminal.setup: Invalid value for auto_close: " .. tostring(v), vim.log.levels.WARN)
449451
end
452+
elseif k == "auto_insert" then
453+
if type(v) == "boolean" then
454+
defaults.auto_insert = v
455+
else
456+
vim.notify("claudecode.terminal.setup: Invalid value for auto_insert: " .. tostring(v), vim.log.levels.WARN)
457+
end
450458
elseif k == "snacks_win_opts" then
451459
if type(v) == "table" then
452460
defaults.snacks_win_opts = v

lua/claudecode/terminal/native.lua

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ local function open_terminal(cmd_string, env_table, effective_config, focus)
5454
if focus then
5555
-- Focus existing terminal: switch to terminal window and enter insert mode
5656
vim.api.nvim_set_current_win(winid)
57-
vim.cmd("startinsert")
57+
if effective_config.auto_insert ~= false then
58+
vim.cmd("startinsert")
59+
end
5860
end
5961
-- If focus=false, preserve user context by staying in current window
6062
return true
@@ -137,7 +139,9 @@ local function open_terminal(cmd_string, env_table, effective_config, focus)
137139
if focus then
138140
-- Focus the terminal: switch to terminal window and enter insert mode
139141
vim.api.nvim_set_current_win(winid)
140-
vim.cmd("startinsert")
142+
if effective_config.auto_insert ~= false then
143+
vim.cmd("startinsert")
144+
end
141145
else
142146
-- Preserve user context: return to the window they were in before terminal creation
143147
vim.api.nvim_set_current_win(original_win)
@@ -164,7 +168,9 @@ end
164168
local function focus_terminal()
165169
if is_valid() then
166170
vim.api.nvim_set_current_win(winid)
167-
vim.cmd("startinsert")
171+
if config.auto_insert ~= false then
172+
vim.cmd("startinsert")
173+
end
168174
end
169175
end
170176

@@ -237,7 +243,9 @@ local function show_hidden_terminal(effective_config, focus)
237243
if focus then
238244
-- Focus the terminal: switch to terminal window and enter insert mode
239245
vim.api.nvim_set_current_win(winid)
240-
vim.cmd("startinsert")
246+
if effective_config.auto_insert ~= false then
247+
vim.cmd("startinsert")
248+
end
241249
else
242250
-- Preserve user context: return to the window they were in before showing terminal
243251
vim.api.nvim_set_current_win(original_win)

lua/claudecode/terminal/snacks.lua

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ end
4848
---@return snacks.terminal.Opts opts Snacks terminal options with start_insert/auto_insert controlled by focus parameter
4949
local function build_opts(config, env_table, focus)
5050
focus = utils.normalize_focus(focus)
51+
local should_insert = focus and config.auto_insert ~= false
5152
return {
5253
env = env_table,
5354
cwd = config.cwd,
54-
start_insert = focus,
55-
auto_insert = focus,
55+
start_insert = should_insert,
56+
auto_insert = should_insert,
5657
auto_close = false,
5758
win = vim.tbl_deep_extend("force", {
5859
position = config.split_side,
@@ -100,26 +101,30 @@ function M.open(cmd_string, env_table, config, focus)
100101
terminal:toggle()
101102
if focus then
102103
terminal:focus()
103-
local term_buf_id = terminal.buf
104-
if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
105-
if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
106-
vim.api.nvim_win_call(terminal.win, function()
107-
vim.cmd("startinsert")
108-
end)
104+
if config.auto_insert ~= false then
105+
local term_buf_id = terminal.buf
106+
if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
107+
if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
108+
vim.api.nvim_win_call(terminal.win, function()
109+
vim.cmd("startinsert")
110+
end)
111+
end
109112
end
110113
end
111114
end
112115
else
113116
-- Terminal is already visible
114117
if focus then
115118
terminal:focus()
116-
local term_buf_id = terminal.buf
117-
if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
118-
-- Check if window is valid before calling nvim_win_call
119-
if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
120-
vim.api.nvim_win_call(terminal.win, function()
121-
vim.cmd("startinsert")
122-
end)
119+
if config.auto_insert ~= false then
120+
local term_buf_id = terminal.buf
121+
if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
122+
-- Check if window is valid before calling nvim_win_call
123+
if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
124+
vim.api.nvim_win_call(terminal.win, function()
125+
vim.cmd("startinsert")
126+
end)
127+
end
123128
end
124129
end
125130
end
@@ -226,11 +231,13 @@ function M.focus_toggle(cmd_string, env_table, config)
226231
else
227232
logger.debug("terminal", "Focus toggle: focusing terminal")
228233
vim.api.nvim_set_current_win(claude_term_neovim_win_id)
229-
if terminal.buf and vim.api.nvim_buf_is_valid(terminal.buf) then
230-
if vim.api.nvim_buf_get_option(terminal.buf, "buftype") == "terminal" then
231-
vim.api.nvim_win_call(claude_term_neovim_win_id, function()
232-
vim.cmd("startinsert")
233-
end)
234+
if config.auto_insert ~= false then
235+
if terminal.buf and vim.api.nvim_buf_is_valid(terminal.buf) then
236+
if vim.api.nvim_buf_get_option(terminal.buf, "buftype") == "terminal" then
237+
vim.api.nvim_win_call(claude_term_neovim_win_id, function()
238+
vim.cmd("startinsert")
239+
end)
240+
end
234241
end
235242
end
236243
end

tests/unit/terminal_spec.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,28 @@ describe("claudecode.terminal (wrapper for Snacks.nvim)", function()
420420
)
421421
end)
422422

423+
it("should store valid auto_insert setting", function()
424+
terminal_wrapper.setup({ auto_insert = false })
425+
terminal_wrapper.open()
426+
local config_arg = mock_snacks_provider.open:get_call(1).refs[3]
427+
assert.are.equal(false, config_arg.auto_insert)
428+
end)
429+
430+
it("should default auto_insert to true", function()
431+
terminal_wrapper.setup({})
432+
terminal_wrapper.open()
433+
local config_arg = mock_snacks_provider.open:get_call(1).refs[3]
434+
assert.are.equal(true, config_arg.auto_insert)
435+
end)
436+
437+
it("should ignore invalid auto_insert and use default", function()
438+
terminal_wrapper.setup({ auto_insert = "invalid" })
439+
terminal_wrapper.open()
440+
local config_arg = mock_snacks_provider.open:get_call(1).refs[3]
441+
assert.are.equal(true, config_arg.auto_insert)
442+
vim.notify:was_called_with(spy.matching.string.match("Invalid value for auto_insert"), vim.log.levels.WARN)
443+
end)
444+
423445
it("should use defaults if user_term_config is not a table and notify", function()
424446
terminal_wrapper.setup("not_a_table")
425447
terminal_wrapper.open()

0 commit comments

Comments
 (0)