Skip to content

Commit abe7493

Browse files
committed
feat: external and TMUX Terminal Provider Support (coder#50)
1 parent 91357d8 commit abe7493

File tree

6 files changed

+136
-13
lines changed

6 files changed

+136
-13
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,27 @@ That's it! The plugin will auto-configure everything else.
8787
- Show diffs with proposed changes
8888
- Access diagnostics and workspace info
8989

90-
## Key Commands
90+
## Commands
91+
92+
- `:ClaudeCode [arguments]` - Toggle the Claude Code terminal window (simple show/hide behavior)
93+
- `:ClaudeCodeFocus [arguments]` - Smart focus/toggle Claude terminal (switches to terminal if not focused, hides if focused)
94+
- `:ClaudeCodeTmux [arguments]` - Open Claude Code in a tmux pane (works regardless of terminal provider setting)
95+
- `:ClaudeCode --resume` - Resume a previous Claude conversation
96+
- `:ClaudeCode --continue` - Continue Claude conversation
97+
- `:ClaudeCodeSend` - Send current visual selection to Claude, or add files from tree explorer
98+
- `:ClaudeCodeTreeAdd` - Add selected file(s) from tree explorer to Claude context (also available via ClaudeCodeSend)
99+
- `:ClaudeCodeAdd <file-path> [start-line] [end-line]` - Add a specific file or directory to Claude context by path with optional line range
100+
- `:ClaudeCodeDiffAccept` - Accept the current diff changes (equivalent to `<leader>aa`)
101+
- `:ClaudeCodeDiffDeny` - Deny/reject the current diff changes (equivalent to `<leader>ad`)
102+
103+
### Toggle Behavior
104+
105+
- **`:ClaudeCode`** - Simple toggle: Always show/hide terminal regardless of current focus
106+
- **`:ClaudeCodeFocus`** - Smart focus: Focus terminal if not active, hide if currently focused
107+
108+
### Tree Integration
109+
110+
The `<leader>as` keybinding has context-aware behavior:
91111

92112
- `:ClaudeCode` - Toggle the Claude Code terminal window
93113
- `:ClaudeCodeFocus` - Smart focus/toggle Claude terminal
@@ -179,6 +199,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
179199
- **Claude not connecting?** Check `:ClaudeCodeStatus` and verify lock file exists in `~/.claude/ide/` (or `$CLAUDE_CONFIG_DIR/ide/` if `CLAUDE_CONFIG_DIR` is set)
180200
- **Need debug logs?** Set `log_level = "debug"` in opts
181201
- **Terminal issues?** Try `provider = "native"` if using snacks.nvim
202+
- **Auto-start not working?** If using external terminal provider, ensure you're using `event = "VeryLazy"` instead of `keys = {...}` only, as lazy loading prevents auto-start from running
182203

183204
## Contributing
184205

lua/claudecode/init.lua

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ M.state = {
8686
---@alias ClaudeCode.TerminalOpts { \
8787
--- split_side?: "left"|"right", \
8888
--- split_width_percentage?: number, \
89-
--- provider?: "auto"|"snacks"|"native", \
89+
--- provider?: "auto"|"snacks"|"native"|"external"|"tmux", \
9090
--- show_native_term_exit_tip?: boolean }
9191
---
9292
---@alias ClaudeCode.SetupOpts { \
@@ -165,9 +165,11 @@ function M._process_queued_mentions()
165165
return
166166
end
167167

168-
-- Ensure terminal is visible when processing queued mentions
168+
-- Ensure terminal is visible when processing queued mentions (unless using external terminal)
169169
local terminal = require("claudecode.terminal")
170-
terminal.ensure_visible()
170+
if not terminal.is_external_provider() then
171+
terminal.ensure_visible()
172+
end
171173

172174
local success_count = 0
173175
local total_count = #mentions_to_send
@@ -258,15 +260,17 @@ function M.send_at_mention(file_path, start_line, end_line, context)
258260

259261
-- Check if Claude Code is connected
260262
if M.is_claude_connected() then
261-
-- Claude is connected, send immediately and ensure terminal is visible
263+
-- Claude is connected, send immediately and ensure terminal is visible (unless using external terminal)
262264
local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line)
263265
if success then
264266
local terminal = require("claudecode.terminal")
265-
terminal.ensure_visible()
267+
if not terminal.is_external_provider() then
268+
terminal.ensure_visible()
269+
end
266270
end
267271
return success, error_msg
268272
else
269-
-- Claude not connected, queue the mention and launch terminal
273+
-- Claude not connected, queue the mention and optionally launch terminal
270274
local mention_data = {
271275
file_path = file_path,
272276
start_line = start_line,
@@ -276,11 +280,15 @@ function M.send_at_mention(file_path, start_line, end_line, context)
276280

277281
queue_at_mention(mention_data)
278282

279-
-- Launch terminal with Claude Code
280283
local terminal = require("claudecode.terminal")
281-
terminal.open()
282-
283-
logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
284+
if terminal.is_external_provider() then
285+
-- Don't launch internal terminal - assume external Claude Code instance exists
286+
logger.debug(context, "Queued @ mention for external Claude Code instance: " .. file_path)
287+
else
288+
-- Launch terminal with Claude Code
289+
terminal.open()
290+
logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
291+
end
284292

285293
return true, nil
286294
end
@@ -496,6 +504,20 @@ function M._create_commands()
496504
vim.api.nvim_create_user_command("ClaudeCodeStatus", function()
497505
if M.state.server and M.state.port then
498506
logger.info("command", "Claude Code integration is running on port " .. tostring(M.state.port))
507+
508+
-- Check if using external terminal provider and provide guidance
509+
local terminal_module_ok, terminal_module = pcall(require, "claudecode.terminal")
510+
if terminal_module_ok and terminal_module then
511+
if terminal_module.is_external_provider() then
512+
local connection_count = M.state.server.get_connection_count and M.state.server.get_connection_count() or 0
513+
if connection_count > 0 then
514+
logger.info("command", "External Claude Code is connected (" .. connection_count .. " connection(s))")
515+
else
516+
logger.info("command", "MCP server ready for external Claude Code connections")
517+
logger.info("command", "Run 'claude --ide' in your terminal to connect to this Neovim instance")
518+
end
519+
end
520+
end
499521
else
500522
logger.info("command", "Claude Code integration is not running")
501523
end
@@ -921,6 +943,26 @@ function M._create_commands()
921943
end, {
922944
desc = "Close the Claude Code terminal window",
923945
})
946+
947+
vim.api.nvim_create_user_command("ClaudeCodeTmux", function(opts)
948+
local tmux_provider = require("claudecode.terminal.tmux")
949+
if not tmux_provider.is_available() then
950+
logger.error("command", "ClaudeCodeTmux: Not running in tmux session")
951+
return
952+
end
953+
954+
-- Use the normal terminal flow but force tmux provider by calling it directly
955+
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
956+
957+
local effective_config = { split_side = "right", split_width_percentage = 0.5 }
958+
local cmd_string, claude_env_table = terminal.get_claude_command_and_env(cmd_args)
959+
960+
tmux_provider.setup({})
961+
tmux_provider.open(cmd_string, claude_env_table, effective_config, true)
962+
end, {
963+
nargs = "*",
964+
desc = "Open Claude Code in new tmux pane (requires tmux session)",
965+
})
924966
else
925967
logger.error(
926968
"init",

lua/claudecode/terminal.lua

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ local function get_provider()
5151
local logger = require("claudecode.logger")
5252

5353
if config.provider == "auto" then
54-
-- Try snacks first, then fallback to native silently
54+
-- Try tmux first if in tmux session, then snacks, then fallback to native silently
55+
local tmux_provider = load_provider("tmux")
56+
if tmux_provider and tmux_provider.is_available() then
57+
logger.debug("terminal", "Auto-detected tmux session, using tmux provider")
58+
return tmux_provider
59+
end
60+
5561
local snacks_provider = load_provider("snacks")
5662
if snacks_provider and snacks_provider.is_available() then
5763
return snacks_provider
@@ -67,6 +73,22 @@ local function get_provider()
6773
elseif config.provider == "native" then
6874
-- noop, will use native provider as default below
6975
logger.debug("terminal", "Using native terminal provider")
76+
elseif config.provider == "tmux" then
77+
local tmux_provider = load_provider("tmux")
78+
if tmux_provider and tmux_provider.is_available() then
79+
logger.debug("terminal", "Using tmux terminal provider")
80+
return tmux_provider
81+
else
82+
logger.warn("terminal", "'tmux' provider configured, but not in tmux session. Falling back to 'native'.")
83+
end
84+
elseif config.provider == "external" then
85+
local external_provider = load_provider("external")
86+
if external_provider then
87+
logger.debug("terminal", "Using external terminal provider")
88+
return external_provider
89+
else
90+
logger.error("terminal", "Failed to load external terminal provider. Falling back to 'native'.")
91+
end
7092
else
7193
logger.warn("terminal", "Invalid provider configured: " .. tostring(config.provider) .. ". Defaulting to 'native'.")
7294
end
@@ -204,7 +226,7 @@ function M.setup(user_term_config, p_terminal_cmd)
204226
config[k] = v
205227
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
206228
config[k] = v
207-
elseif k == "provider" and (v == "snacks" or v == "native") then
229+
elseif k == "provider" and (v == "snacks" or v == "native" or v == "external" or v == "tmux") then
208230
config[k] = v
209231
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
210232
config[k] = v
@@ -286,6 +308,26 @@ function M.get_active_terminal_bufnr()
286308
return get_provider().get_active_bufnr()
287309
end
288310

311+
--- Checks if the current terminal provider is external.
312+
-- @return boolean True if using external terminal provider, false otherwise.
313+
function M.is_external_provider()
314+
return config.provider == "external"
315+
end
316+
317+
--- Checks if the current terminal provider is tmux.
318+
-- @return boolean True if using tmux terminal provider, false otherwise.
319+
function M.is_tmux_provider()
320+
return config.provider == "tmux"
321+
end
322+
323+
--- Gets the claude command and environment variables for external use.
324+
-- @param cmd_args string|nil Optional arguments to append to the command
325+
-- @return string cmd_string The command string
326+
-- @return table env_table The environment variables table
327+
function M.get_claude_command_and_env(cmd_args)
328+
return get_claude_command_and_env(cmd_args)
329+
end
330+
289331
--- Gets the managed terminal instance for testing purposes.
290332
-- NOTE: This function is intended for use in tests to inspect internal state.
291333
-- The underscore prefix indicates it's not part of the public API for regular use.

tests/unit/claudecode_add_command_spec.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ describe("ClaudeCodeAdd command", function()
9090
return 1
9191
end,
9292
simple_toggle = spy.new(function() end),
93+
is_external_provider = function()
94+
return false -- Default to false for existing tests
95+
end,
96+
is_tmux_provider = function()
97+
return false -- Default to false for existing tests
98+
end,
9399
}
94100
elseif mod == "claudecode.visual_commands" then
95101
return {

tests/unit/claudecode_send_command_spec.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ describe("ClaudeCodeSend Command Range Functionality", function()
7272
mock_terminal = {
7373
open = spy.new(function() end),
7474
ensure_visible = spy.new(function() end),
75+
is_external_provider = function()
76+
return false -- Default to false for existing tests
77+
end,
78+
is_tmux_provider = function()
79+
return false -- Default to false for existing tests
80+
end,
7581
}
7682

7783
-- Mock server

tests/unit/init_spec.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ describe("claudecode.init", function()
301301
close = spy.new(function() end),
302302
setup = spy.new(function() end),
303303
ensure_visible = spy.new(function() end),
304+
is_external_provider = function()
305+
return false -- Default to false for existing tests
306+
end,
307+
is_tmux_provider = function()
308+
return false -- Default to false for existing tests
309+
end,
304310
}
305311

306312
local original_require = _G.require

0 commit comments

Comments
 (0)