Skip to content

Commit 4d61264

Browse files
committed
feat: add focus_after_send option for terminal behavior
Change-Id: Iff065cea29c3935b76be2c78cd27e0bd9e9f9854 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent e1945e6 commit 4d61264

File tree

6 files changed

+147
-1
lines changed

6 files changed

+147
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
249249
-- For local installations: "~/.claude/local/claude"
250250
-- For native binary: use output from 'which claude'
251251

252+
-- Send/Focus Behavior
253+
-- When true, successful sends will focus the Claude terminal if already connected
254+
focus_after_send = false,
255+
252256
-- Selection Tracking
253257
track_selection = true,
254258
visual_demotion_delay_ms = 50,

dev-config.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ return {
4848
-- log_level = "info", -- "trace", "debug", "info", "warn", "error"
4949
-- terminal_cmd = nil, -- Custom terminal command (default: "claude")
5050

51+
-- Send/Focus Behavior
52+
focus_after_send = true, -- Focus Claude terminal after successful send while connected
53+
5154
-- Selection Tracking
5255
-- track_selection = true, -- Enable real-time selection tracking
5356
-- visual_demotion_delay_ms = 50, -- Delay before demoting visual selection (ms)

lua/claudecode/config.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ M.defaults = {
1414
env = {}, -- Custom environment variables for Claude terminal
1515
log_level = "info",
1616
track_selection = true,
17+
-- When true, focus Claude terminal after a successful send while connected
18+
focus_after_send = false,
1719
visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection
1820
connection_wait_delay = 200, -- Milliseconds to wait after connection before sending queued @ mentions
1921
connection_timeout = 10000, -- Maximum time to wait for Claude Code to connect (milliseconds)
@@ -84,6 +86,7 @@ function M.validate(config)
8486
assert(is_valid_log_level, "log_level must be one of: " .. table.concat(valid_log_levels, ", "))
8587

8688
assert(type(config.track_selection) == "boolean", "track_selection must be a boolean")
89+
assert(type(config.focus_after_send) == "boolean", "focus_after_send must be a boolean")
8790

8891
assert(
8992
type(config.visual_demotion_delay_ms) == "number" and config.visual_demotion_delay_ms >= 0,

lua/claudecode/init.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,12 @@ function M.send_at_mention(file_path, start_line, end_line, context)
269269
local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line)
270270
if success then
271271
local terminal = require("claudecode.terminal")
272-
terminal.ensure_visible()
272+
if M.state.config and M.state.config.focus_after_send then
273+
-- Open focuses the terminal without toggling/hiding if already focused
274+
terminal.open()
275+
else
276+
terminal.ensure_visible()
277+
end
273278
end
274279
return success, error_msg
275280
else

lua/claudecode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
---@field env table<string, string>
108108
---@field log_level ClaudeCodeLogLevel
109109
---@field track_selection boolean
110+
---@field focus_after_send boolean
110111
---@field visual_demotion_delay_ms number
111112
---@field connection_wait_delay number
112113
---@field connection_timeout number

tests/unit/focus_after_send_spec.lua

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
require("tests.busted_setup")
2+
require("tests.mocks.vim")
3+
4+
describe("focus_after_send behavior", function()
5+
local saved_require
6+
local claudecode
7+
8+
local mock_terminal
9+
local mock_logger
10+
local mock_server_facade
11+
12+
local function setup_mocks(focus_after_send)
13+
mock_terminal = {
14+
setup = function() end,
15+
open = spy.new(function() end),
16+
ensure_visible = spy.new(function() end),
17+
}
18+
19+
mock_logger = {
20+
setup = function() end,
21+
debug = function() end,
22+
info = function() end,
23+
warn = function() end,
24+
error = function() end,
25+
}
26+
27+
mock_server_facade = {
28+
broadcast = spy.new(function()
29+
return true
30+
end),
31+
}
32+
33+
local mock_config = {
34+
apply = function()
35+
-- Return only fields used in this test path
36+
return {
37+
auto_start = false,
38+
terminal_cmd = nil,
39+
env = {},
40+
log_level = "info",
41+
track_selection = false,
42+
focus_after_send = focus_after_send,
43+
diff_opts = {
44+
layout = "vertical",
45+
open_in_new_tab = false,
46+
keep_terminal_focus = false,
47+
on_new_file_reject = "keep_empty",
48+
},
49+
models = { { name = "Claude Sonnet 4 (Latest)", value = "sonnet" } },
50+
}
51+
end,
52+
}
53+
54+
saved_require = _G.require
55+
_G.require = function(mod)
56+
if mod == "claudecode.config" then
57+
return mock_config
58+
elseif mod == "claudecode.logger" then
59+
return mock_logger
60+
elseif mod == "claudecode.diff" then
61+
return { setup = function() end }
62+
elseif mod == "claudecode.terminal" then
63+
return mock_terminal
64+
elseif mod == "claudecode.server.init" then
65+
return {
66+
get_status = function()
67+
return { running = true, client_count = 1 }
68+
end,
69+
}
70+
else
71+
return saved_require(mod)
72+
end
73+
end
74+
end
75+
76+
local function teardown_mocks()
77+
_G.require = saved_require
78+
package.loaded["claudecode"] = nil
79+
package.loaded["claudecode.config"] = nil
80+
package.loaded["claudecode.logger"] = nil
81+
package.loaded["claudecode.diff"] = nil
82+
package.loaded["claudecode.terminal"] = nil
83+
package.loaded["claudecode.server.init"] = nil
84+
end
85+
86+
after_each(function()
87+
teardown_mocks()
88+
end)
89+
90+
it("focuses terminal with open() when enabled", function()
91+
setup_mocks(true)
92+
93+
claudecode = require("claudecode")
94+
claudecode.setup({})
95+
96+
-- Mark server as present and stub low-level broadcast to succeed
97+
claudecode.state.server = mock_server_facade
98+
claudecode._broadcast_at_mention = spy.new(function()
99+
return true, nil
100+
end)
101+
102+
-- Act
103+
local ok, err = claudecode.send_at_mention("/tmp/file.lua", nil, nil, "test")
104+
assert.is_true(ok)
105+
assert.is_nil(err)
106+
107+
-- Assert focus behavior
108+
assert.spy(mock_terminal.open).was_called()
109+
assert.spy(mock_terminal.ensure_visible).was_not_called()
110+
end)
111+
112+
it("only ensures visibility when disabled (default)", function()
113+
setup_mocks(false)
114+
115+
claudecode = require("claudecode")
116+
claudecode.setup({})
117+
118+
claudecode.state.server = mock_server_facade
119+
claudecode._broadcast_at_mention = spy.new(function()
120+
return true, nil
121+
end)
122+
123+
local ok, err = claudecode.send_at_mention("/tmp/file.lua", nil, nil, "test")
124+
assert.is_true(ok)
125+
assert.is_nil(err)
126+
127+
assert.spy(mock_terminal.ensure_visible).was_called()
128+
assert.spy(mock_terminal.open).was_not_called()
129+
end)
130+
end)

0 commit comments

Comments
 (0)