Skip to content

feat: add focus_after_send option for terminal behavior #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: thomask33/feat_add_working_directory_control_for_Claude_terminal
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
-- For local installations: "~/.claude/local/claude"
-- For native binary: use output from 'which claude'

-- Send/Focus Behavior
-- When true, successful sends will focus the Claude terminal if already connected
focus_after_send = false,

-- Selection Tracking
track_selection = true,
visual_demotion_delay_ms = 50,
Expand Down
3 changes: 3 additions & 0 deletions dev-config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ return {
-- log_level = "info", -- "trace", "debug", "info", "warn", "error"
-- terminal_cmd = nil, -- Custom terminal command (default: "claude")

-- Send/Focus Behavior
focus_after_send = true, -- Focus Claude terminal after successful send while connected

-- Selection Tracking
-- track_selection = true, -- Enable real-time selection tracking
-- visual_demotion_delay_ms = 50, -- Delay before demoting visual selection (ms)
Expand Down
3 changes: 3 additions & 0 deletions lua/claudecode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ M.defaults = {
env = {}, -- Custom environment variables for Claude terminal
log_level = "info",
track_selection = true,
-- When true, focus Claude terminal after a successful send while connected
focus_after_send = false,
visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection
connection_wait_delay = 200, -- Milliseconds to wait after connection before sending queued @ mentions
connection_timeout = 10000, -- Maximum time to wait for Claude Code to connect (milliseconds)
Expand Down Expand Up @@ -84,6 +86,7 @@ function M.validate(config)
assert(is_valid_log_level, "log_level must be one of: " .. table.concat(valid_log_levels, ", "))

assert(type(config.track_selection) == "boolean", "track_selection must be a boolean")
assert(type(config.focus_after_send) == "boolean", "focus_after_send must be a boolean")

assert(
type(config.visual_demotion_delay_ms) == "number" and config.visual_demotion_delay_ms >= 0,
Expand Down
7 changes: 6 additions & 1 deletion lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,12 @@ function M.send_at_mention(file_path, start_line, end_line, context)
local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line)
if success then
local terminal = require("claudecode.terminal")
terminal.ensure_visible()
if M.state.config and M.state.config.focus_after_send then
-- Open focuses the terminal without toggling/hiding if already focused
terminal.open()
else
terminal.ensure_visible()
end
end
return success, error_msg
else
Expand Down
1 change: 1 addition & 0 deletions lua/claudecode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
---@field env table<string, string>
---@field log_level ClaudeCodeLogLevel
---@field track_selection boolean
---@field focus_after_send boolean
---@field visual_demotion_delay_ms number
---@field connection_wait_delay number
---@field connection_timeout number
Expand Down
130 changes: 130 additions & 0 deletions tests/unit/focus_after_send_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
require("tests.busted_setup")
require("tests.mocks.vim")

describe("focus_after_send behavior", function()
local saved_require
local claudecode

local mock_terminal
local mock_logger
local mock_server_facade

local function setup_mocks(focus_after_send)
mock_terminal = {
setup = function() end,
open = spy.new(function() end),
ensure_visible = spy.new(function() end),
}

mock_logger = {
setup = function() end,
debug = function() end,
info = function() end,
warn = function() end,
error = function() end,
}

mock_server_facade = {
broadcast = spy.new(function()
return true
end),
}

local mock_config = {
apply = function()
-- Return only fields used in this test path
return {
auto_start = false,
terminal_cmd = nil,
env = {},
log_level = "info",
track_selection = false,
focus_after_send = focus_after_send,
diff_opts = {
layout = "vertical",
open_in_new_tab = false,
keep_terminal_focus = false,
on_new_file_reject = "keep_empty",
},
models = { { name = "Claude Sonnet 4 (Latest)", value = "sonnet" } },
}
end,
}

saved_require = _G.require
_G.require = function(mod)
if mod == "claudecode.config" then
return mock_config
elseif mod == "claudecode.logger" then
return mock_logger
elseif mod == "claudecode.diff" then
return { setup = function() end }
elseif mod == "claudecode.terminal" then
return mock_terminal
elseif mod == "claudecode.server.init" then
return {
get_status = function()
return { running = true, client_count = 1 }
end,
}
else
return saved_require(mod)
end
end
end

local function teardown_mocks()
_G.require = saved_require
package.loaded["claudecode"] = nil
package.loaded["claudecode.config"] = nil
package.loaded["claudecode.logger"] = nil
package.loaded["claudecode.diff"] = nil
package.loaded["claudecode.terminal"] = nil
package.loaded["claudecode.server.init"] = nil
end

after_each(function()
teardown_mocks()
end)

it("focuses terminal with open() when enabled", function()
setup_mocks(true)

claudecode = require("claudecode")
claudecode.setup({})

-- Mark server as present and stub low-level broadcast to succeed
claudecode.state.server = mock_server_facade
claudecode._broadcast_at_mention = spy.new(function()
return true, nil
end)

-- Act
local ok, err = claudecode.send_at_mention("/tmp/file.lua", nil, nil, "test")
assert.is_true(ok)
assert.is_nil(err)

-- Assert focus behavior
assert.spy(mock_terminal.open).was_called()
assert.spy(mock_terminal.ensure_visible).was_not_called()
end)

it("only ensures visibility when disabled (default)", function()
setup_mocks(false)

claudecode = require("claudecode")
claudecode.setup({})

claudecode.state.server = mock_server_facade
claudecode._broadcast_at_mention = spy.new(function()
return true, nil
end)

local ok, err = claudecode.send_at_mention("/tmp/file.lua", nil, nil, "test")
assert.is_true(ok)
assert.is_nil(err)

assert.spy(mock_terminal.ensure_visible).was_called()
assert.spy(mock_terminal.open).was_not_called()
end)
end)