Skip to content

feat: external and TMUX Terminal Provider Support #50

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 5 commits 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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ That's it! The plugin will auto-configure everything else.
## Key Commands

- `:ClaudeCode` - Toggle the Claude Code terminal window
- `:ClaudeCodeTmux [arguments]` - Open Claude Code in a tmux pane (works regardless of terminal provider setting)
- `:ClaudeCodeFocus` - Smart focus/toggle Claude terminal
- `:ClaudeCodeSend` - Send current visual selection to Claude
- `:ClaudeCodeAdd <file-path> [start-line] [end-line]` - Add specific file to Claude context with optional line range
Expand Down Expand Up @@ -155,7 +156,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
terminal = {
split_side = "right", -- "left" or "right"
split_width_percentage = 0.30,
provider = "auto", -- "auto", "snacks", or "native"
provider = "auto", -- "auto", "snacks", "native", "external" or "tmux"
auto_close = true,
},

Expand All @@ -179,6 +180,7 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
- **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
- **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

## Contributing

Expand Down
62 changes: 52 additions & 10 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ M.state = {
---@alias ClaudeCode.TerminalOpts { \
--- split_side?: "left"|"right", \
--- split_width_percentage?: number, \
--- provider?: "auto"|"snacks"|"native", \
--- provider?: "auto"|"snacks"|"native"|"external"|"tmux", \
--- show_native_term_exit_tip?: boolean }
---
---@alias ClaudeCode.SetupOpts { \
Expand Down Expand Up @@ -165,9 +165,11 @@ function M._process_queued_mentions()
return
end

-- Ensure terminal is visible when processing queued mentions
-- Ensure terminal is visible when processing queued mentions (unless using external terminal)
local terminal = require("claudecode.terminal")
terminal.ensure_visible()
if not terminal.is_external_provider() then
terminal.ensure_visible()
end

local success_count = 0
local total_count = #mentions_to_send
Expand Down Expand Up @@ -258,15 +260,17 @@ 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 (unless using external terminal)
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 not terminal.is_external_provider() then
terminal.ensure_visible()
end
end
return success, error_msg
else
-- Claude not connected, queue the mention and launch terminal
-- Claude not connected, queue the mention and optionally launch terminal
local mention_data = {
file_path = file_path,
start_line = start_line,
Expand All @@ -276,11 +280,15 @@ 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()

logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
if terminal.is_external_provider() then
-- Don't launch internal terminal - assume external Claude Code instance exists
logger.debug(context, "Queued @ mention for external Claude Code instance: " .. file_path)
else
-- Launch terminal with Claude Code
terminal.open()
logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)
end

return true, nil
end
Expand Down Expand Up @@ -496,6 +504,20 @@ function M._create_commands()
vim.api.nvim_create_user_command("ClaudeCodeStatus", function()
if M.state.server and M.state.port then
logger.info("command", "Claude Code integration is running on port " .. tostring(M.state.port))

-- Check if using external terminal provider and provide guidance
local terminal_module_ok, terminal_module = pcall(require, "claudecode.terminal")
if terminal_module_ok and terminal_module then
if terminal_module.is_external_provider() then
local connection_count = M.state.server.get_connection_count and M.state.server.get_connection_count() or 0
if connection_count > 0 then
logger.info("command", "External Claude Code is connected (" .. connection_count .. " connection(s))")
else
logger.info("command", "MCP server ready for external Claude Code connections")
logger.info("command", "Run 'claude --ide' in your terminal to connect to this Neovim instance")
end
end
end
else
logger.info("command", "Claude Code integration is not running")
end
Expand Down Expand Up @@ -921,6 +943,26 @@ function M._create_commands()
end, {
desc = "Close the Claude Code terminal window",
})

vim.api.nvim_create_user_command("ClaudeCodeTmux", function(opts)
local tmux_provider = require("claudecode.terminal.tmux")
if not tmux_provider.is_available() then
logger.error("command", "ClaudeCodeTmux: Not running in tmux session")
return
end

-- Use the normal terminal flow but force tmux provider by calling it directly
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil

local effective_config = { split_side = "right", split_width_percentage = 0.5 }
local cmd_string, claude_env_table = terminal.get_claude_command_and_env(cmd_args)

tmux_provider.setup({})
tmux_provider.open(cmd_string, claude_env_table, effective_config, true)
end, {
nargs = "*",
desc = "Open Claude Code in new tmux pane (requires tmux session)",
})
else
logger.error(
"init",
Expand Down
46 changes: 44 additions & 2 deletions lua/claudecode/terminal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ local function get_provider()
local logger = require("claudecode.logger")

if config.provider == "auto" then
-- Try snacks first, then fallback to native silently
-- Try tmux first if in tmux session, then snacks, then fallback to native silently
local tmux_provider = load_provider("tmux")
if tmux_provider and tmux_provider.is_available() then
logger.debug("terminal", "Auto-detected tmux session, using tmux provider")
return tmux_provider
end

local snacks_provider = load_provider("snacks")
if snacks_provider and snacks_provider.is_available() then
return snacks_provider
Expand All @@ -67,6 +73,22 @@ local function get_provider()
elseif config.provider == "native" then
-- noop, will use native provider as default below
logger.debug("terminal", "Using native terminal provider")
elseif config.provider == "tmux" then
local tmux_provider = load_provider("tmux")
if tmux_provider and tmux_provider.is_available() then
logger.debug("terminal", "Using tmux terminal provider")
return tmux_provider
else
logger.warn("terminal", "'tmux' provider configured, but not in tmux session. Falling back to 'native'.")
end
elseif config.provider == "external" then
local external_provider = load_provider("external")
if external_provider then
logger.debug("terminal", "Using external terminal provider")
return external_provider
else
logger.error("terminal", "Failed to load external terminal provider. Falling back to 'native'.")
end
else
logger.warn("terminal", "Invalid provider configured: " .. tostring(config.provider) .. ". Defaulting to 'native'.")
end
Expand Down Expand Up @@ -204,7 +226,7 @@ function M.setup(user_term_config, p_terminal_cmd)
config[k] = v
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
config[k] = v
elseif k == "provider" and (v == "snacks" or v == "native") then
elseif k == "provider" and (v == "snacks" or v == "native" or v == "external" or v == "tmux") then
config[k] = v
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
config[k] = v
Expand Down Expand Up @@ -286,6 +308,26 @@ function M.get_active_terminal_bufnr()
return get_provider().get_active_bufnr()
end

--- Checks if the current terminal provider is external.
-- @return boolean True if using external terminal provider, false otherwise.
function M.is_external_provider()
return config.provider == "external"
end

--- Checks if the current terminal provider is tmux.
-- @return boolean True if using tmux terminal provider, false otherwise.
function M.is_tmux_provider()
return config.provider == "tmux"
end

--- Gets the claude command and environment variables for external use.
-- @param cmd_args string|nil Optional arguments to append to the command
-- @return string cmd_string The command string
-- @return table env_table The environment variables table
function M.get_claude_command_and_env(cmd_args)
return get_claude_command_and_env(cmd_args)
end

--- Gets the managed terminal instance for testing purposes.
-- NOTE: This function is intended for use in tests to inspect internal state.
-- The underscore prefix indicates it's not part of the public API for regular use.
Expand Down
72 changes: 72 additions & 0 deletions lua/claudecode/terminal/external.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
--- External terminal provider for Claude Code.
-- This provider does nothing - it assumes Claude Code is running in an external terminal.
-- @module claudecode.terminal.external

--- @type TerminalProvider
local M = {}

--- Configures the external terminal provider (no-op).
-- @param term_config table The terminal configuration (ignored).
function M.setup(term_config)
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Opens the Claude terminal (no-op for external provider).
-- @param cmd_string string The command to run (ignored).
-- @param env_table table Environment variables (ignored).
-- @param effective_config table Terminal configuration (ignored).
-- @param focus boolean|nil Whether to focus the terminal (ignored).
function M.open(cmd_string, env_table, effective_config, focus)
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Closes the managed Claude terminal (no-op for external provider).
function M.close()
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Simple toggle: show/hide the Claude terminal (no-op for external provider).
-- @param cmd_string string The command to run (ignored).
-- @param env_table table Environment variables (ignored).
-- @param effective_config table Terminal configuration (ignored).
function M.simple_toggle(cmd_string, env_table, effective_config)
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Smart focus toggle: switches to terminal if not focused, hides if currently focused (no-op for external provider).
-- @param cmd_string string The command to run (ignored).
-- @param env_table table Environment variables (ignored).
-- @param effective_config table Terminal configuration (ignored).
function M.focus_toggle(cmd_string, env_table, effective_config)
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Toggles the Claude terminal open or closed (no-op for external provider).
-- @param cmd_string string The command to run (ignored).
-- @param env_table table Environment variables (ignored).
-- @param effective_config table Terminal configuration (ignored).
function M.toggle(cmd_string, env_table, effective_config)
-- Intentionally left blank - external provider assumes Claude Code is running elsewhere
end

--- Gets the buffer number of the currently active Claude Code terminal.
-- For external provider, this always returns nil since there's no managed terminal.
-- @return nil Always returns nil for external provider.
function M.get_active_bufnr()
return nil
end

--- Checks if the external terminal provider is available.
-- The external provider is always available.
-- @return boolean Always returns true.
function M.is_available()
return true
end

--- Gets the managed terminal instance for testing purposes (external provider has none).
-- @return nil Always returns nil for external provider.
function M._get_terminal_for_test()
return nil
end

return M
Loading
Loading