diff --git a/lua/claudecode/config.lua b/lua/claudecode/config.lua index 573fc4c..d6c71c7 100644 --- a/lua/claudecode/config.lua +++ b/lua/claudecode/config.lua @@ -6,6 +6,7 @@ M.defaults = { port_range = { min = 10000, max = 65535 }, auto_start = true, terminal_cmd = nil, + enable_terminal = true, -- Enable built-in terminal integration (set to false to use external terminal like tmux) log_level = "info", track_selection = true, visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection @@ -51,6 +52,8 @@ function M.validate(config) assert(type(config.track_selection) == "boolean", "track_selection must be a boolean") + assert(type(config.enable_terminal) == "boolean", "enable_terminal must be a boolean") + assert( type(config.visual_demotion_delay_ms) == "number" and config.visual_demotion_delay_ms >= 0, "visual_demotion_delay_ms must be a non-negative number" diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index f673899..8015b60 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -260,7 +260,7 @@ function M.send_at_mention(file_path, start_line, end_line, context) if M.is_claude_connected() then -- Claude is connected, send immediately and ensure terminal is visible local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line) - if success then + if success and M.state.config.enable_terminal then local terminal = require("claudecode.terminal") terminal.ensure_visible() end @@ -276,9 +276,11 @@ function M.send_at_mention(file_path, start_line, end_line, context) queue_at_mention(mention_data) - -- Launch terminal with Claude Code - local terminal = require("claudecode.terminal") - terminal.open() + -- Launch terminal with Claude Code if enabled + if M.state.config.enable_terminal then + local terminal = require("claudecode.terminal") + terminal.open() + end logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path) @@ -307,15 +309,17 @@ function M.setup(opts) -- Setup terminal module: always try to call setup to pass terminal_cmd, -- even if terminal_opts (for split_side etc.) are not provided. - local terminal_setup_ok, terminal_module = pcall(require, "claudecode.terminal") - if terminal_setup_ok then - -- Guard in case tests or user replace the module with a minimal stub without `setup`. - if type(terminal_module.setup) == "function" then - -- terminal_opts might be nil, which the setup function should handle gracefully. - terminal_module.setup(terminal_opts, M.state.config.terminal_cmd) + if M.state.config.enable_terminal then + local terminal_setup_ok, terminal_module = pcall(require, "claudecode.terminal") + if terminal_setup_ok then + -- Guard in case tests or user replace the module with a minimal stub without `setup`. + if type(terminal_module.setup) == "function" then + -- terminal_opts might be nil, which the setup function should handle gracefully. + terminal_module.setup(terminal_opts, M.state.config.terminal_cmd) + end + else + logger.error("init", "Failed to load claudecode.terminal module for setup.") end - else - logger.error("init", "Failed to load claudecode.terminal module for setup.") end local diff = require("claudecode.diff") @@ -882,50 +886,52 @@ function M._create_commands() desc = "Add specified file or directory to Claude Code context with optional line range", }) - local terminal_ok, terminal = pcall(require, "claudecode.terminal") - if terminal_ok then - vim.api.nvim_create_user_command("ClaudeCode", function(opts) - local current_mode = vim.fn.mode() - if current_mode == "v" or current_mode == "V" or current_mode == "\22" then - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false) - end - local cmd_args = opts.args and opts.args ~= "" and opts.args or nil - terminal.simple_toggle({}, cmd_args) - end, { - nargs = "*", - desc = "Toggle the Claude Code terminal window (simple show/hide) with optional arguments", - }) - - vim.api.nvim_create_user_command("ClaudeCodeFocus", function(opts) - local current_mode = vim.fn.mode() - if current_mode == "v" or current_mode == "V" or current_mode == "\22" then - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false) - end - local cmd_args = opts.args and opts.args ~= "" and opts.args or nil - terminal.focus_toggle({}, cmd_args) - end, { - nargs = "*", - desc = "Smart focus/toggle Claude Code terminal (switches to terminal if not focused, hides if focused)", - }) - - vim.api.nvim_create_user_command("ClaudeCodeOpen", function(opts) - local cmd_args = opts.args and opts.args ~= "" and opts.args or nil - terminal.open({}, cmd_args) - end, { - nargs = "*", - desc = "Open the Claude Code terminal window with optional arguments", - }) - - vim.api.nvim_create_user_command("ClaudeCodeClose", function() - terminal.close() - end, { - desc = "Close the Claude Code terminal window", - }) - else - logger.error( - "init", - "Terminal module not found. Terminal commands (ClaudeCode, ClaudeCodeOpen, ClaudeCodeClose) not registered." - ) + if M.state.config.enable_terminal then + local terminal_ok, terminal = pcall(require, "claudecode.terminal") + if terminal_ok then + vim.api.nvim_create_user_command("ClaudeCode", function(opts) + local current_mode = vim.fn.mode() + if current_mode == "v" or current_mode == "V" or current_mode == "\22" then + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false) + end + local cmd_args = opts.args and opts.args ~= "" and opts.args or nil + terminal.simple_toggle({}, cmd_args) + end, { + nargs = "*", + desc = "Toggle the Claude Code terminal window (simple show/hide) with optional arguments", + }) + + vim.api.nvim_create_user_command("ClaudeCodeFocus", function(opts) + local current_mode = vim.fn.mode() + if current_mode == "v" or current_mode == "V" or current_mode == "\22" then + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false) + end + local cmd_args = opts.args and opts.args ~= "" and opts.args or nil + terminal.focus_toggle({}, cmd_args) + end, { + nargs = "*", + desc = "Smart focus/toggle Claude Code terminal (switches to terminal if not focused, hides if focused)", + }) + + vim.api.nvim_create_user_command("ClaudeCodeOpen", function(opts) + local cmd_args = opts.args and opts.args ~= "" and opts.args or nil + terminal.open({}, cmd_args) + end, { + nargs = "*", + desc = "Open the Claude Code terminal window with optional arguments", + }) + + vim.api.nvim_create_user_command("ClaudeCodeClose", function() + terminal.close() + end, { + desc = "Close the Claude Code terminal window", + }) + else + logger.error( + "init", + "Terminal module not found. Terminal commands (ClaudeCode, ClaudeCodeOpen, ClaudeCodeClose) not registered." + ) + end end -- Diff management commands diff --git a/tests/config_test.lua b/tests/config_test.lua index 9b4aaec..c40217e 100644 --- a/tests/config_test.lua +++ b/tests/config_test.lua @@ -180,6 +180,7 @@ describe("Config module", function() port_range = { min = 10000, max = 65535 }, auto_start = true, terminal_cmd = "toggleterm", + enable_terminal = true, log_level = "debug", track_selection = false, visual_demotion_delay_ms = 50, diff --git a/tests/integration/command_args_spec.lua b/tests/integration/command_args_spec.lua index 05787c0..8adcd92 100644 --- a/tests/integration/command_args_spec.lua +++ b/tests/integration/command_args_spec.lua @@ -149,9 +149,13 @@ describe("ClaudeCode command arguments integration", function() port_range = { min = 10000, max = 65535 }, auto_start = false, terminal_cmd = nil, + enable_terminal = true, log_level = "info", track_selection = true, visual_demotion_delay_ms = 50, + connection_wait_delay = 200, + connection_timeout = 10000, + queue_timeout = 5000, diff_opts = { auto_close_on_accept = true, show_diff_stats = true, diff --git a/tests/unit/config_spec.lua b/tests/unit/config_spec.lua index 0bada03..62829e2 100644 --- a/tests/unit/config_spec.lua +++ b/tests/unit/config_spec.lua @@ -29,6 +29,7 @@ describe("Configuration", function() port_range = { min = 10000, max = 65535 }, auto_start = true, terminal_cmd = "toggleterm", + enable_terminal = true, log_level = "debug", track_selection = false, visual_demotion_delay_ms = 50, diff --git a/tests/unit/enable_terminal_spec.lua b/tests/unit/enable_terminal_spec.lua new file mode 100644 index 0000000..34ac3d0 --- /dev/null +++ b/tests/unit/enable_terminal_spec.lua @@ -0,0 +1,251 @@ +require("tests.busted_setup") +require("tests.mocks.vim") + +describe("enable_terminal configuration", function() + -- Test config validation (no mocking needed) + describe("config validation", function() + it("should accept boolean true for enable_terminal", function() + local config = require("claudecode.config") + local valid_config = { + port_range = { min = 10000, max = 65535 }, + auto_start = true, + enable_terminal = true, + log_level = "info", + track_selection = true, + visual_demotion_delay_ms = 50, + connection_wait_delay = 200, + connection_timeout = 10000, + queue_timeout = 5000, + diff_opts = { + auto_close_on_accept = true, + show_diff_stats = true, + vertical_split = true, + open_in_current_tab = true, + }, + } + + local success = pcall(config.validate, valid_config) + assert.is_true(success) + end) + + it("should accept boolean false for enable_terminal", function() + local config = require("claudecode.config") + local valid_config = { + port_range = { min = 10000, max = 65535 }, + auto_start = true, + enable_terminal = false, + log_level = "info", + track_selection = true, + visual_demotion_delay_ms = 50, + connection_wait_delay = 200, + connection_timeout = 10000, + queue_timeout = 5000, + diff_opts = { + auto_close_on_accept = true, + show_diff_stats = true, + vertical_split = true, + open_in_current_tab = true, + }, + } + + local success = pcall(config.validate, valid_config) + assert.is_true(success) + end) + + it("should reject non-boolean values for enable_terminal", function() + local config = require("claudecode.config") + local invalid_config = { + port_range = { min = 10000, max = 65535 }, + auto_start = true, + enable_terminal = "true", -- Invalid: string instead of boolean + log_level = "info", + track_selection = true, + visual_demotion_delay_ms = 50, + connection_wait_delay = 200, + connection_timeout = 10000, + queue_timeout = 5000, + diff_opts = { + auto_close_on_accept = true, + show_diff_stats = true, + vertical_split = true, + open_in_current_tab = true, + }, + } + + local success, err = pcall(config.validate, invalid_config) + assert.is_false(success) + assert.is_not_nil(string.find(tostring(err), "enable_terminal must be a boolean")) + end) + + it("should have enable_terminal = true as default", function() + local config = require("claudecode.config") + assert.is_true(config.defaults.enable_terminal) + end) + end) + + -- Test command registration based on config + describe("command registration", function() + local original_nvim_create_user_command + + before_each(function() + -- Clear any loaded modules + package.loaded["claudecode"] = nil + package.loaded["claudecode.server.init"] = nil + package.loaded["claudecode.terminal"] = nil + + -- Save original and set up spy + original_nvim_create_user_command = vim.api.nvim_create_user_command + spy.on(vim.api, "nvim_create_user_command") + + -- Mock minimal server functionality + package.loaded["claudecode.server.init"] = { + start = function() + return true, 12345 + end, + stop = function() + return true + end, + } + + -- Mock lockfile + package.loaded["claudecode.lockfile"] = { + create = function() + return true, nil, "test-token" + end, + remove = function() + return true + end, + generate_auth_token = function() + return "test-auth-token" + end, + } + + -- Mock other required modules minimally + package.loaded["claudecode.selection"] = { + enable = function() end, + disable = function() end, + } + + package.loaded["claudecode.diff"] = { + setup = function() end, + } + + package.loaded["claudecode.logger"] = { + setup = function() end, + info = function() end, + warn = function() end, + error = function() end, + debug = function() end, + } + end) + + after_each(function() + -- Restore original function + vim.api.nvim_create_user_command = original_nvim_create_user_command + end) + + it("should register terminal commands when enable_terminal is true", function() + -- Mock terminal module + package.loaded["claudecode.terminal"] = { + setup = function() end, + open = function() end, + close = function() end, + simple_toggle = function() end, + focus_toggle = function() end, + ensure_visible = function() end, + } + + local claudecode = require("claudecode") + claudecode.setup({ + auto_start = false, + enable_terminal = true, + }) + + local registered_commands = {} + for _, call in ipairs(vim.api.nvim_create_user_command.calls) do + registered_commands[call.vals[1]] = true + end + + assert.is_true(registered_commands["ClaudeCode"] ~= nil, "ClaudeCode command should be registered") + assert.is_true(registered_commands["ClaudeCodeOpen"] ~= nil, "ClaudeCodeOpen command should be registered") + assert.is_true(registered_commands["ClaudeCodeClose"] ~= nil, "ClaudeCodeClose command should be registered") + assert.is_true(registered_commands["ClaudeCodeFocus"] ~= nil, "ClaudeCodeFocus command should be registered") + end) + + it("should NOT register terminal commands when enable_terminal is false", function() + -- Mock terminal module + package.loaded["claudecode.terminal"] = { + setup = function() end, + open = function() end, + close = function() end, + simple_toggle = function() end, + focus_toggle = function() end, + ensure_visible = function() end, + } + + local claudecode = require("claudecode") + claudecode.setup({ + auto_start = false, + enable_terminal = false, + }) + + local registered_commands = {} + for _, call in ipairs(vim.api.nvim_create_user_command.calls) do + registered_commands[call.vals[1]] = true + end + + assert.is_nil(registered_commands["ClaudeCode"], "ClaudeCode command should NOT be registered") + assert.is_nil(registered_commands["ClaudeCodeOpen"], "ClaudeCodeOpen command should NOT be registered") + assert.is_nil(registered_commands["ClaudeCodeClose"], "ClaudeCodeClose command should NOT be registered") + assert.is_nil(registered_commands["ClaudeCodeFocus"], "ClaudeCodeFocus command should NOT be registered") + end) + + it("should NOT call terminal.setup when enable_terminal is false", function() + local terminal_setup_called = false + + -- Mock terminal module with tracking + package.loaded["claudecode.terminal"] = { + setup = function() + terminal_setup_called = true + end, + open = function() end, + close = function() end, + simple_toggle = function() end, + focus_toggle = function() end, + ensure_visible = function() end, + } + + local claudecode = require("claudecode") + claudecode.setup({ + auto_start = false, + enable_terminal = false, + }) + + assert.is_false(terminal_setup_called, "terminal.setup should NOT be called when enable_terminal is false") + end) + + it("should call terminal.setup when enable_terminal is true", function() + local terminal_setup_called = false + + -- Mock terminal module with tracking + package.loaded["claudecode.terminal"] = { + setup = function() + terminal_setup_called = true + end, + open = function() end, + close = function() end, + simple_toggle = function() end, + focus_toggle = function() end, + ensure_visible = function() end, + } + + local claudecode = require("claudecode") + claudecode.setup({ + auto_start = false, + enable_terminal = true, + }) + + assert.is_true(terminal_setup_called, "terminal.setup should be called when enable_terminal is true") + end) + end) +end)