diff --git a/README.md b/README.md index 004faf0..f86a568 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/dev-config.lua b/dev-config.lua index 8dc6910..6939f92 100644 --- a/dev-config.lua +++ b/dev-config.lua @@ -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) diff --git a/lua/claudecode/config.lua b/lua/claudecode/config.lua index a3cd70c..5ff412d 100644 --- a/lua/claudecode/config.lua +++ b/lua/claudecode/config.lua @@ -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) @@ -85,6 +87,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, diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index 763aa02..36e4703 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -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 diff --git a/lua/claudecode/types.lua b/lua/claudecode/types.lua index b00dcd1..f696ce9 100644 --- a/lua/claudecode/types.lua +++ b/lua/claudecode/types.lua @@ -108,6 +108,7 @@ ---@field env table ---@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 diff --git a/tests/unit/focus_after_send_spec.lua b/tests/unit/focus_after_send_spec.lua new file mode 100644 index 0000000..8fe57a1 --- /dev/null +++ b/tests/unit/focus_after_send_spec.lua @@ -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)