Skip to content

feat(terminal): add toggle to enable/disable the built-in terminal #99

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: main
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
split_width_percentage = 0.30,
provider = "auto", -- "auto", "snacks", "native", or custom provider table
auto_close = true,
enable = true, -- Enable terminal commands (set to false when using external Claude Code)
snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()` - see Floating Window section below
},

Expand Down Expand Up @@ -572,11 +573,38 @@ Provides convenient Claude interaction history management and access for enhance

> **Disclaimer**: These community extensions are developed and maintained by independent contributors. The authors and their extensions are not affiliated with Coder. Use at your own discretion and refer to their respective repositories for installation instructions, documentation, and support.

## Using External Claude Code

If you prefer to run Claude Code externally (e.g., in a separate terminal), you can disable the plugin's built-in terminal commands:

```lua
{
"coder/claudecode.nvim",
opts = {
terminal = {
enable = false, -- Disable ClaudeCode, ClaudeCodeOpen, etc.
},
},
}
```

> **💡 Tip**: **Disable lazy loading** (`lazy = false`) for the best experience with external Claude Code. This ensures the WebSocket server starts immediately when Neovim opens, allowing instant connection from external terminals.

Then connect your external Claude Code instance using:

```bash
# Start claudecode.nvim first: :ClaudeCodeStart in Neovim (or auto-starts if lazy = false)
claude --ide # Connects to the running Neovim WebSocket server
```

This approach gives you full control over Claude Code's terminal interface while still providing all the IDE integration features.

## Troubleshooting

- **Claude not connecting?** Check `:ClaudeCodeStatus` and verify lock file exists in `~/.claude/ide/` (or `$CLAUDE_CONFIG_DIR/ide/` if `CLAUDE_CONFIG_DIR` is set)
- **Need debug logs?** Set `log_level = "debug"` in opts
- **Terminal issues?** Try `provider = "native"` if using snacks.nvim
- **Want to use external Claude Code?** Set `terminal = { enable = false }`, then use `claude --ide` command
- **Local installation not working?** If you used `claude migrate-installer`, set `terminal_cmd = "~/.claude/local/claude"` in your config. Check `which claude` vs `ls ~/.claude/local/claude` to verify your installation type.
- **Native binary installation not working?** If you used the alpha native binary installer, run `claude doctor` to verify installation health and use `which claude` to find the binary path. Set `terminal_cmd = "/path/to/claude"` with the detected path in your config.

Expand Down
30 changes: 20 additions & 10 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ M.version = {
--- @field connection_timeout number Maximum time to wait for Claude Code to connect (milliseconds).
--- @field queue_timeout number Maximum time to keep @ mentions in queue (milliseconds).
--- @field diff_opts { auto_close_on_accept: boolean, show_diff_stats: boolean, vertical_split: boolean, open_in_current_tab: boolean } Options for the diff provider.
--- @field enable_terminal boolean Whether to enable terminal commands (ClaudeCode, ClaudeCodeOpen, etc.).

--- @type ClaudeCode.Config
local default_config = {
Expand All @@ -63,6 +64,7 @@ local default_config = {
vertical_split = true,
open_in_current_tab = false,
},
enable_terminal = true,
}

--- @class ClaudeCode.State
Expand Down Expand Up @@ -324,22 +326,25 @@ function M.send_at_mention(file_path, start_line, end_line, context)

-- Check if Claude Code is connected
if M.is_claude_connected() then
-- Claude is connected, send immediately and ensure terminal is visible
-- Claude is connected, send immediately and ensure terminal is visible (if enabled)
local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line)
if success then
local terminal = require("claudecode.terminal")
local terminal_ok, terminal = pcall(require, "claudecode.terminal")
if success and terminal_ok and terminal.is_enabled() then
terminal.ensure_visible()
end
return success, error_msg
else
-- Claude not connected, queue the mention and launch terminal
-- Claude not connected, queue the mention and launch terminal (if enabled)
queue_mention(file_path, start_line, end_line)

-- Launch terminal with Claude Code
local terminal = require("claudecode.terminal")
terminal.open()

logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
local terminal_ok, terminal = pcall(require, "claudecode.terminal")
if terminal_ok and terminal.is_enabled() then
-- Launch terminal with Claude Code
terminal.open()
logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
else
logger.debug(context, "Queued @ mention (terminal disabled): " .. file_path)
end

return true, nil
end
Expand Down Expand Up @@ -990,7 +995,7 @@ function M._create_commands()
})

local terminal_ok, terminal = pcall(require, "claudecode.terminal")
if terminal_ok then
if terminal_ok and terminal.is_enabled() 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
Expand Down Expand Up @@ -1028,6 +1033,11 @@ function M._create_commands()
end, {
desc = "Close the Claude Code terminal window",
})
elseif terminal_ok and not terminal.is_enabled() then
logger.debug(
"init",
"Terminal commands disabled via terminal.enable = false. Terminal commands (ClaudeCode, ClaudeCodeOpen, ClaudeCodeClose) not registered."
)
else
logger.error(
"init",
Expand Down
9 changes: 9 additions & 0 deletions lua/claudecode/terminal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ local config = {
show_native_term_exit_tip = true,
terminal_cmd = nil,
auto_close = true,
enable = true, -- Enable terminal commands (ClaudeCode, ClaudeCodeOpen, etc.)
env = {}, -- Custom environment variables for Claude terminal
snacks_win_opts = {},
}
Expand Down Expand Up @@ -313,6 +314,8 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
config[k] = v
elseif k == "auto_close" and type(v) == "boolean" then
config[k] = v
elseif k == "enable" and type(v) == "boolean" then
config[k] = v
elseif k == "snacks_win_opts" and type(v) == "table" then
config[k] = v
else
Expand Down Expand Up @@ -403,4 +406,10 @@ function M._get_managed_terminal_for_test()
return nil
end

--- Gets whether terminal commands are enabled
-- @return boolean True if terminal commands are enabled
function M.is_enabled()
return config.enable
end

return M
1 change: 1 addition & 0 deletions tests/unit/claudecode_add_command_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe("ClaudeCodeAdd command", function()
return 1
end,
simple_toggle = spy.new(function() end),
is_enabled = function() return true end,
}
elseif mod == "claudecode.visual_commands" then
return {
Expand Down
1 change: 1 addition & 0 deletions tests/unit/claudecode_send_command_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe("ClaudeCodeSend Command Range Functionality", function()
mock_terminal = {
open = spy.new(function() end),
ensure_visible = spy.new(function() end),
is_enabled = function() return true end,
}

-- Mock server
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/init_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ describe("claudecode.init", function()
close = spy.new(function() end),
setup = spy.new(function() end),
ensure_visible = spy.new(function() end),
is_enabled = function() return true end,
}

local original_require = _G.require
Expand Down Expand Up @@ -475,6 +476,7 @@ describe("claudecode.init", function()
focus_toggle = spy.new(function() end),
open = spy.new(function() end),
close = spy.new(function() end),
is_enabled = function() return true end,
}

-- Mock vim.ui.select to automatically select the first model
Expand Down
1 change: 1 addition & 0 deletions tests/unit/visual_delay_timing_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe("Visual Delay Timing Validation", function()
get_active_terminal_bufnr = function()
return nil -- No active terminal by default
end,
is_enabled = function() return true end,
}

-- Extend the existing vim mock
Expand Down