Skip to content
Closed
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: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ This plugin was built entirely with Claude Code in a Neovim terminal, and then i
- 🔄 Automatically detect and reload files modified by Claude Code
- ⚡ Real-time buffer updates when files are changed externally
- 📱 Customizable window position and size (including floating windows)
- 🎯 **Visual selection support** - Send selected code to Claude with context
- 🤖 Integration with which-key (if available)
- 📂 Automatically uses git project root as working directory (when available)
- 🧩 Modular and maintainable code structure
- 📋 Type annotations with LuaCATS for better IDE support
- ✅ Configuration validation to prevent errors
- 🧪 Testing framework for reliability (44 comprehensive tests)
- 🧪 Testing framework for reliability (60 comprehensive tests)

## Requirements

Expand Down Expand Up @@ -146,6 +147,10 @@ require("claude-code").setup({
verbose = "<leader>cV", -- Normal mode keymap for Claude Code with verbose flag
},
},
-- Visual mode selection keymaps
selection = {
ask = "<leader>cs", -- Ask about selection with prompt input
},
window_navigation = true, -- Enable window navigation keymaps (<C-h/j/k/l>)
scrolling = true, -- Enable scrolling keymaps (<C-f/b>) for page up/down
}
Expand Down Expand Up @@ -198,6 +203,10 @@ Variant mode mappings (if configured):
- `<leader>cC` - Toggle Claude Code with --continue flag
- `<leader>cV` - Toggle Claude Code with --verbose flag

Visual selection mapping (select code in visual mode, then use):

- `<leader>cs` - Ask Claude about the selection (prompts for input)

Additionally, when in the Claude Code terminal:

- `<C-h>` - Move to the window on the left
Expand All @@ -211,6 +220,23 @@ Note: After scrolling with `<C-f>` or `<C-b>`, you'll need to press the `i` key

When Claude Code modifies files that are open in Neovim, they'll be automatically reloaded.

### Lua API

The plugin exposes a Lua API for programmatic control:

```lua
local claude = require("claude-code")

-- Basic controls
claude.toggle() -- Toggle terminal visibility
claude.open() -- Open/show terminal
claude.close() -- Hide terminal (keeps session)

-- Send text to Claude
claude.send("Hello Claude!") -- Send raw text
claude.send("Fix this bug\r") -- With carriage return to submit
```

### Floating Window Example

To use Claude Code in a floating window:
Expand Down
59 changes: 59 additions & 0 deletions lua/claude-code/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ local M = {}
-- @field normal string|boolean Normal mode keymap for toggling Claude Code, false to disable
-- @field terminal string|boolean Terminal mode keymap for toggling Claude Code, false to disable

--- ClaudeCodeKeymapsSelection class for visual selection keymap configuration
-- @table ClaudeCodeKeymapsSelection
-- @field ask string|boolean Visual mode keymap to ask about selection with prompt input

--- ClaudeCodeKeymaps class for keymap configuration
-- @table ClaudeCodeKeymaps
-- @field toggle ClaudeCodeKeymapsToggle Keymaps for toggling Claude Code
-- @field selection ClaudeCodeKeymapsSelection Keymaps for visual selection
-- @field window_navigation boolean Enable window navigation keymaps
-- @field scrolling boolean Enable scrolling keymaps

Expand All @@ -60,12 +65,18 @@ local M = {}
-- @field pushd_cmd string Command to push directory onto stack (e.g., 'pushd' for bash/zsh)
-- @field popd_cmd string Command to pop directory from stack (e.g., 'popd' for bash/zsh)

--- ClaudeCodeTmux class for tmux integration configuration
-- @table ClaudeCodeTmux
-- @field enable boolean Enable tmux integration (check for Claude Code in tmux panes first)
-- @field prefer_tmux boolean When true, prefer tmux pane over nvim terminal if both exist

--- ClaudeCodeConfig class for main configuration
-- @table ClaudeCodeConfig
-- @field window ClaudeCodeWindow Terminal window settings
-- @field refresh ClaudeCodeRefresh File refresh settings
-- @field git ClaudeCodeGit Git integration settings
-- @field shell ClaudeCodeShell Shell-specific configuration
-- @field tmux ClaudeCodeTmux Tmux integration settings
-- @field command string Command used to launch Claude Code
-- @field command_variants ClaudeCodeCommandVariants Command variants configuration
-- @field keymaps ClaudeCodeKeymaps Keymaps configuration
Expand Down Expand Up @@ -110,6 +121,11 @@ M.default_config = {
pushd_cmd = 'pushd', -- Command to push directory onto stack
popd_cmd = 'popd', -- Command to pop directory from stack
},
-- Tmux integration settings
tmux = {
enable = true, -- Enable tmux integration (check for Claude Code in tmux panes first)
prefer_tmux = true, -- When true, prefer tmux pane over nvim terminal if both exist
},
-- Command settings
command = 'claude', -- Command used to launch Claude Code
-- Command variants
Expand All @@ -131,6 +147,10 @@ M.default_config = {
verbose = '<leader>cV', -- Normal mode keymap for Claude Code with verbose flag
},
},
-- Visual mode selection keymaps
selection = {
ask = '<leader>cs', -- Visual mode keymap to ask about selection with prompt input
},
window_navigation = true, -- Enable window navigation keymaps (<C-h/j/k/l>)
scrolling = true, -- Enable scrolling keymaps (<C-f/b>) for page up/down
},
Expand Down Expand Up @@ -316,6 +336,26 @@ local function validate_shell_config(shell)
return true, nil
end

--- Validate tmux configuration
--- @param tmux table Tmux configuration
--- @return boolean valid
--- @return string? error_message
local function validate_tmux_config(tmux)
if type(tmux) ~= 'table' then
return false, 'tmux config must be a table'
end

if type(tmux.enable) ~= 'boolean' then
return false, 'tmux.enable must be a boolean'
end

if type(tmux.prefer_tmux) ~= 'boolean' then
return false, 'tmux.prefer_tmux must be a boolean'
end

return true, nil
end

--- Validate keymaps configuration
--- @param keymaps table Keymaps configuration
--- @return boolean valid
Expand Down Expand Up @@ -359,6 +399,19 @@ local function validate_keymaps_config(keymaps)
return false, 'keymaps.scrolling must be a boolean'
end

-- Validate selection keymaps if they exist
if keymaps.selection then
if type(keymaps.selection) ~= 'table' then
return false, 'keymaps.selection must be a table'
end

if keymaps.selection.ask ~= nil then
if not (keymaps.selection.ask == false or type(keymaps.selection.ask) == 'string') then
return false, 'keymaps.selection.ask must be a string or false'
end
end
end

return true, nil
end

Expand Down Expand Up @@ -418,6 +471,12 @@ local function validate_config(config)
return false, err
end

-- Validate tmux settings
valid, err = validate_tmux_config(config.tmux)
if not valid then
return false, err
end

-- Validate command settings
if type(config.command) ~= 'string' then
return false, 'command must be a string'
Expand Down
104 changes: 104 additions & 0 deletions lua/claude-code/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ local file_refresh = require('claude-code.file_refresh')
local terminal = require('claude-code.terminal')
local git = require('claude-code.git')
local version = require('claude-code.version')
local tmux = require('claude-code.tmux')

local M = {}

-- Make imported modules available
M.commands = commands
M.tmux = tmux

-- Store the current configuration
--- @type table
Expand Down Expand Up @@ -103,6 +105,108 @@ end
--- Version information
M.version = version

--- Send raw text to the Claude Code terminal or tmux pane
--- @param text string Text to send to the terminal
--- @return boolean success True if text was sent successfully
function M.send(text)
-- Check tmux first if enabled
if M.config.tmux and M.config.tmux.enable then
local tmux_pane = tmux.find_claude_pane()
if tmux_pane then
-- If prefer_tmux is true, or nvim terminal is not running, use tmux
if M.config.tmux.prefer_tmux or not M.claude_code.current_instance then
return tmux.send_text(tmux_pane, text)
end
end
end

-- Fall back to nvim terminal
-- Ensure Claude Code is running
if not M.claude_code.current_instance then
-- Start Claude Code first
M.toggle()
-- Wait a bit for terminal to initialize
vim.defer_fn(function()
terminal.send_text(M, text)
end, 100)
return true
end

return terminal.send_text(M, text)
end

--- Send raw text followed by Enter to the Claude Code terminal or tmux pane
--- @param text string Text to send
--- @return boolean success True if text was sent successfully
function M.send_with_enter(text)
-- Check tmux first if enabled
if M.config.tmux and M.config.tmux.enable then
local tmux_pane = tmux.find_claude_pane()
if tmux_pane then
if M.config.tmux.prefer_tmux or not M.claude_code.current_instance then
return tmux.send_text_with_enter(tmux_pane, text)
end
end
end

-- Fall back to nvim terminal
if not M.send(text) then
return false
end
return M.send('\r')
end

--- Check if Claude Code is available (either in tmux or nvim terminal)
--- @return boolean available True if Claude Code is available
--- @return string source "tmux" or "nvim" indicating where Claude Code is running
function M.is_available()
-- Check tmux first if enabled
if M.config.tmux and M.config.tmux.enable then
if tmux.find_claude_pane() then
return true, 'tmux'
end
end

-- Check nvim terminal
if M.claude_code.current_instance then
local bufnr = M.claude_code.instances[M.claude_code.current_instance]
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
return true, 'nvim'
end
end

return false, nil
end

--- Open Claude Code and focus the terminal window
--- @return boolean success True if window is now visible
function M.open()
if not M.claude_code.current_instance then
M.toggle()
return true
end

return terminal.ensure_visible(M, M.config)
end
Comment on lines +181 to +190
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return value is misleading when starting a new instance.

When there's no current instance, open() returns true immediately after calling M.toggle(), but the terminal needs time to initialize. This suggests success before the terminal is actually ready for use (e.g., before send() can be called successfully).

Additionally, the function returns inconsistent types: literal true for new instances vs. the result of terminal.ensure_visible() for existing instances.

Consider either:

  1. Returning false when starting a new instance to indicate "not yet ready"
  2. Documenting that true means "initiated" rather than "ready"
  3. Making the return value consistent by always returning a boolean indicating current visibility state
 function M.open()
   if not M.claude_code.current_instance then
-    M.toggle()
-    return true
+    M.toggle()  
+    return false  -- Terminal is starting but not yet ready for send()
   end
 
   return terminal.ensure_visible(M, M.config)
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--- Open Claude Code and focus the terminal window
--- @return boolean success True if window is now visible
function M.open()
if not M.claude_code.current_instance then
M.toggle()
return true
end
return terminal.ensure_visible(M, M.config)
end
--- Open Claude Code and focus the terminal window
--- @return boolean success True if window is now visible
function M.open()
if not M.claude_code.current_instance then
M.toggle()
return false -- Terminal is starting but not yet ready for send()
end
return terminal.ensure_visible(M, M.config)
end
🤖 Prompt for AI Agents
In lua/claude-code/init.lua around lines 119 to 128, the function returns a
literal true when creating a new instance which misrepresents readiness and
produces inconsistent return types; change behavior so the function always
returns the actual visibility/readiness boolean by calling M.toggle() to start
the terminal and then returning terminal.ensure_visible(M, M.config) (i.e.,
don't return true immediately — invoke ensure_visible after toggling and return
its boolean result so callers get a consistent, accurate readiness state).


--- Close the Claude Code terminal window (hide, not terminate)
function M.close()
local instance_id = M.claude_code.current_instance
if not instance_id then
return
end

local bufnr = M.claude_code.instances[instance_id]
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
return
end

local win_ids = vim.fn.win_findbuf(bufnr)
for _, win_id in ipairs(win_ids) do
vim.api.nvim_win_close(win_id, true)
end
end

--- Setup function for the plugin
--- @param user_config? table User configuration table (optional)
function M.setup(user_config)
Expand Down
Loading