Skip to content

Conversation

@dukjjang
Copy link

@dukjjang dukjjang commented Nov 25, 2025

Summary

This PR adds visual selection support, allowing users to select code in visual mode and send it to Claude Code with a custom prompt.

Key Features

  • Visual mode keymap (<leader>cs): Select code, press the keymap, enter a prompt, and the selection context is sent to Claude Code
  • Smart context sending: Instead of copying the entire selection, it sends only the file path and line range (e.g., See /path/to/file.lua:45-49), allowing Claude Code to read the file directly for better context
  • New session detection: Automatically detects if Claude Code is starting fresh and waits for initialization (2 seconds) before sending the prompt
  • Lua API additions: send(), open(), close() functions for programmatic control

Configuration

require("claude-code").setup({
  keymaps = {
    selection = {
      ask = "<leader>cs",  -- or false to disable
    },
  },
})

How It Works

  1. User selects code in visual mode
  2. User presses <leader>cs
  3. A prompt input appears asking "Ask Claude:"
  4. User types their question (e.g., "Explain this function")
  5. The plugin sends to Claude Code:
    See /path/to/file.lua:45-49
    
    Explain this function
    
  6. Claude Code reads the file and responds with context

Files Changed

File Changes
terminal.lua Added send_text(), get_job_id(), ensure_visible()
init.lua Added send(), open(), close() public API
keymaps.lua Added visual selection keymap registration
config.lua Added keymaps.selection.ask configuration
README.md Updated documentation
tests/spec/selection_spec.lua Added 12 new tests

Test Plan

  • All existing tests pass
  • New selection tests pass (12 tests)
  • Manual testing: visual selection → prompt input → Claude Code receives message
  • Manual testing: new session detection works correctly
  • Manual testing: existing session sends immediately

This is my first contribution to this project. Please let me know if there are any issues or if you'd like me to make any changes. I'm happy to address any feedback!

Summary by CodeRabbit

  • New Features

    • Visual selection: select code in visual mode, prompt Claude, and send selection via a new configurable keybinding (default cs).
    • tmux integration & availability: Claude Code can send text via tmux when available, plus new programmatic controls (send, send with Enter, open, close, is_available, ensure visible).
  • Documentation

    • Added visual selection usage, mapping notes, and Lua API examples.
  • Tests

    • Expanded test coverage (now 60 tests).

✏️ Tip: You can customize this high-level summary in your review settings.

Add the ability to select code in visual mode and send it to Claude Code
with a custom prompt. This feature sends only the file path and line range,
allowing Claude Code to read the file directly for better context.

New features:
- Visual mode keymap (<leader>cs) to ask about selected code
- Lua API: send(), open(), close() for programmatic control
- Auto-detection of new session with appropriate delay for initialization

Configuration:
- keymaps.selection.ask: customize the visual mode keymap (default: <leader>cs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Adds visual-selection support that prompts Claude from visual mode, new tmux integration and terminal helper APIs, new public Lua functions to send/open/close Claude Code, config extensions (keymaps.selection and tmux settings), README updates, and a new selection-focused test suite.

Changes

Cohort / File(s) Summary
Configuration
lua/claude-code/config.lua
Add keymaps.selection type and default (ask = '<leader>cs'); add tmux config (enable, prefer_tmux); add validation for tmux and keymaps.selection; merge into parsed config.
Public API / Lifecycle
lua/claude-code/init.lua
Expose M.tmux; add M.send(text), M.send_with_enter(text), M.is_available(), M.open(), and M.close() to route text via tmux or Neovim terminal and manage UI lifecycle.
Terminal helpers
lua/claude-code/terminal.lua
Add get_job_id(claude_code), send_text(claude_code, text), and ensure_visible(claude_code, config) to look up job IDs, send text to terminal, and ensure the Claude Code window is visible.
Tmux integration
lua/claude-code/tmux.lua
New module exposing tmux helpers: is_in_tmux(), process inspection, find_claude_pane(), get_current_pane(), send_text(...), send_text_with_enter(...), focus_pane(...), and is_claude_available().
Visual selection keymap
lua/claude-code/keymaps.lua
Add visual-mode mapping (config.keymaps.selection.ask) that captures file:range, prompts "Ask Claude: ", composes the message, starts/toggles instance if needed, waits for CLI readiness, sends text and Enter; registers which-key entry when available.
Documentation
README.md
Document visual selection feature and mapping, visual selection usage, Lua API examples (toggle/open/close/send), tmux notes, and update test count to 60.
Tests
tests/spec/selection_spec.lua
Add tests mocking Neovim APIs to validate selection capture, terminal/tmux interactions, init/send/open/close flows, and job/send/ensure_visible behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant VisualKeymap as Visual Keymap
    participant Prompt as Prompt
    participant Init as Init Module
    participant Tmux as tmux Module
    participant Terminal as Terminal

    User->>VisualKeymap: Press visual mapping (<leader>cs)
    VisualKeymap->>VisualKeymap: Capture selection & file:range
    VisualKeymap->>Prompt: Prompt "Ask Claude: "
    Prompt->>VisualKeymap: Return user input
    alt input provided
        VisualKeymap->>Init: Check availability (M.is_available)
        alt available via tmux
            Init->>Tmux: find_claude_pane / focus_pane
            note right of Tmux `#e8fff2`: short ready wait (~200ms)
            Tmux->>Tmux: send_text_with_enter(pane, message)
        else available via nvim terminal or start needed
            Init->>Terminal: ensure_visible(config)
            note right of Terminal `#fff4e6`: startup wait (~2000ms) if new
            Terminal->>Terminal: send_text(instance, message)
            Terminal->>Terminal: send Enter
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • lua/claude-code/keymaps.lua — selection capture, prompting, wait_for_cli_ready and scheduling behavior.
    • lua/claude-code/tmux.lua — system calls, process inspection, pane discovery, and escaping for send-keys.
    • lua/claude-code/init.lua — routing between tmux and terminal, lifecycle (open/close) correctness.
    • lua/claude-code/terminal.lua — job-id handling and safe send semantics.
    • tests/spec/selection_spec.lua — adequacy of mocks for integration branches.

"🐰
I hop and scope the highlighted line,
I whisper file:ranges, make them shine,
Open, send, a carrot-sent note,
Claude replies — my whiskers gloat,
Tests wiggle — all builds align."

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature added: visual selection support for sending code context to Claude.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (2)

8-8: Inconsistent test count in badge.

The badge still shows "44 passing" but line 39 states "60 comprehensive tests". Update the badge to reflect the actual test count.

-[![Tests](https://img.shields.io/badge/Tests-44%20passing-success?style=flat-square&logo=github-actions)](https://github.com/greggh/claude-code.nvim/actions/workflows/ci.yml)
+[![Tests](https://img.shields.io/badge/Tests-60%20passing-success?style=flat-square&logo=github-actions)](https://github.com/greggh/claude-code.nvim/actions/workflows/ci.yml)

287-289: Update test count in development section.

This section still mentions "44 comprehensive tests" but should be updated to 60 to match line 39.

-- Complete installation instructions for all platforms in [DEVELOPMENT.md](DEVELOPMENT.md)
 - Pre-commit hooks for code quality
-- Testing framework with 44 comprehensive tests
+- Testing framework with 60 comprehensive tests
 - Linting and formatting tools
🧹 Nitpick comments (5)
lua/claude-code/keymaps.lua (2)

55-66: Handle edge case for unsaved buffers.

If the buffer has no file path (unsaved buffer), vim.fn.expand('%:p') returns an empty string, resulting in a message like "See :1-5" which is not helpful to Claude.

Consider checking for this case:

       -- Capture file info BEFORE vim.ui.input (which exits visual mode)
       local filepath = vim.fn.expand('%:p')
+      if filepath == '' then
+        vim.notify('Cannot send selection from unsaved buffer', vim.log.levels.WARN)
+        return
+      end
       -- Use visual mode marks (current selection)
       local start_line = vim.fn.line('v')

79-92: Consider making initialization delay configurable.

The hardcoded 2000ms delay for new sessions may not be sufficient on slower systems or too long on fast ones. Consider exposing this as a configuration option in a future iteration.

For now, the implementation is acceptable, but verify the 2000ms delay works reliably across different systems and Claude Code versions.

tests/spec/selection_spec.lua (3)

12-135: Extract common mock setup to reduce duplication.

The mock setup for vim APIs is duplicated almost identically between the two test describe blocks. Consider extracting this into a shared helper function to improve maintainability and reduce test code bloat.

-- Example approach: Create a helper that returns a fresh mock environment
local function setup_vim_mocks()
  _G.vim = _G.vim or {}
  _G.vim.api = _G.vim.api or {}
  _G.vim.fn = _G.vim.fn or {}
  -- ... rest of mocks
  return _G.vim
end

-- Then in before_each:
before_each(function()
  setup_vim_mocks()
  -- ... test-specific setup
end)

Also applies to: 206-296


393-416: Expand test coverage for send/open/close to verify actual behavior.

The tests for init.send(), init.open(), and init.close() are minimal and only verify happy-path scenarios. Consider adding tests for:

  • send(): empty text, text with special characters, when terminal is not running
  • open(): verify window is actually made visible (not just returns true), state after calling open
  • close(): verify a window was actually closed, state after calling close, behavior when no window is open

160-177: Add edge case tests for send_text and send functions.

The send_text tests don't cover scenarios such as:

  • Sending empty strings
  • Handling special characters or newlines in text
  • Terminal sending text when disconnected/stale

Adding these would increase confidence in error handling and boundary conditions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9a31e5 and e792803.

📒 Files selected for processing (6)
  • README.md (4 hunks)
  • lua/claude-code/config.lua (3 hunks)
  • lua/claude-code/init.lua (1 hunks)
  • lua/claude-code/keymaps.lua (2 hunks)
  • lua/claude-code/terminal.lua (1 hunks)
  • tests/spec/selection_spec.lua (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-02T19:40:53.423Z
Learnt from: CR
Repo: greggh/claude-code.nvim PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-07-02T19:40:53.423Z
Learning: Applies to lua/claude-code*/**/*.lua : Main plugin code should reside in '/lua/claude-code', with subdirectories for CLI integration, UI components, and context management

Applied to files:

  • lua/claude-code/terminal.lua
  • lua/claude-code/init.lua
  • README.md
🧬 Code graph analysis (1)
lua/claude-code/init.lua (1)
lua/claude-code/terminal.lua (1)
  • M.toggle (451-477)
🔇 Additional comments (13)
README.md (4)

33-39: LGTM!

The new visual selection feature and updated test count are well documented.


150-156: LGTM!

The new selection.ask configuration option is clearly documented with the default keymap.


206-208: LGTM!

Visual selection mapping documentation is clear and helpful.


223-238: LGTM!

The new Lua API section provides clear examples for programmatic control.

lua/claude-code/terminal.lua (3)

383-398: LGTM!

The get_job_id function correctly handles nil checks for instance_id and buffer validation before accessing the terminal job ID.


400-414: LGTM!

The send_text function properly validates the job ID and provides user feedback when the terminal isn't running.


416-445: LGTM!

The ensure_visible function correctly checks for existing windows before creating new ones, avoiding duplicate windows.

lua/claude-code/config.lua (3)

42-49: LGTM!

Type annotations for the new ClaudeCodeKeymapsSelection class are well-defined and consistent with existing patterns.


139-142: LGTM!

Default configuration for visual selection keymap follows the existing pattern and matches the documentation.


371-382: LGTM!

Validation logic correctly handles the optional keymaps.selection table and validates ask as either a string or false.

lua/claude-code/keymaps.lua (1)

127-134: LGTM!

The which-key registration for the selection keymap follows the established pattern.

lua/claude-code/init.lua (2)

124-133: LGTM!

The open() function correctly handles both starting a new instance and ensuring visibility of an existing one.


135-151: LGTM!

The close() function correctly hides the terminal window without terminating the session, and handles edge cases gracefully.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
lua/claude-code/init.lua (1)

106-117: Document timing requirement or provide async API for open() → send() sequence.

The existing code in keymaps.lua correctly handles the timing concern by using vim.defer_fn delays (2 seconds for new sessions, 200ms otherwise) before calling send() after open(). However, this timing requirement isn't documented in the function's comments, creating maintenance risk and poor API clarity for future callers.

Either:

  1. Add documentation to send() clarifying that the terminal must be fully initialized after open() (typically requiring 200-2000ms depending on context)
  2. Or integrate the async pattern into the API itself (e.g., make open() return a future/coroutine, or add send_deferred())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e792803 and 190ad3f.

📒 Files selected for processing (1)
  • lua/claude-code/init.lua (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-02T19:40:53.423Z
Learnt from: CR
Repo: greggh/claude-code.nvim PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-07-02T19:40:53.423Z
Learning: Applies to lua/claude-code*/**/*.lua : Main plugin code should reside in '/lua/claude-code', with subdirectories for CLI integration, UI components, and context management

Applied to files:

  • lua/claude-code/init.lua
🧬 Code graph analysis (1)
lua/claude-code/init.lua (1)
lua/claude-code/terminal.lua (1)
  • M.toggle (451-477)
🔇 Additional comments (1)
lua/claude-code/init.lua (1)

130-146: LGTM!

The implementation correctly handles edge cases (missing instance, invalid buffer) and properly closes all windows displaying the Claude Code buffer. Using force=true is appropriate for terminal windows.

Comment on lines +119 to +133
--- 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
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).

@dukjjang dukjjang force-pushed the feature/visual-selection-support branch from 190ad3f to e792803 Compare November 25, 2025 02:34
- Replace fixed delay with dynamic CLI ready detection
- Monitor terminal buffer for ">" prompt pattern
- Poll every 50ms with 2.5s timeout fallback
- Add unsaved buffer validation check
- Existing sessions execute immediately without waiting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@dukjjang
Copy link
Author

Thanks for the detailed review! I've addressed the feedback in commit 1de1c01:

Changes Made

1. Replaced fixed delay with dynamic CLI ready detection

Instead of using hardcoded delays (2000ms/200ms), the code now monitors the terminal buffer content and detects when Claude Code CLI shows the > prompt, indicating it's ready for input.

  • Polls every 50ms with a 2.5 second timeout fallback
  • Existing sessions execute immediately without waiting
  • New sessions wait for CLI prompt detection

2. Added unsaved buffer validation

Added check for empty filepath to prevent sending selections from unsaved buffers, as suggested in the review.

3. Timing/async concerns

The open()send() sequence timing is now handled by the wait_for_cli_ready() function which monitors actual CLI state rather than relying on arbitrary delays.

Let me know if there are any other concerns!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
lua/claude-code/keymaps.lua (1)

137-160: Clarify / tighten is_new_session vs claude.open() contract

is_new_session is computed before calling claude.open() based on claude.claude_code.current_instance and instances[current_instance], and then passed into wait_for_cli_ready. This assumes claude.open() will not spin up a fresh CLI session when those fields are non‑nil; if open() can restart or swap the underlying CLI while keeping current_instance/instances truthy, wait_for_cli_ready will skip polling and may send input before the new CLI is actually ready.

If open() guarantees that "truthy current_instance + instances[idx] implies no new CLI process is started", consider documenting that invariant here in a short comment. If not, consider plumbing an explicit is_new_session/started_new_cli flag from open() (or recomputing after open()) so wait_for_cli_ready always gets an accurate value.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 190ad3f and 1de1c01.

📒 Files selected for processing (1)
  • lua/claude-code/keymaps.lua (2 hunks)
🔇 Additional comments (2)
lua/claude-code/keymaps.lua (2)

55-164: Visual selection flow and CLI-ready polling look solid

The end-to-end flow (unsaved-buffer guard, visual range capture, prompt input, CLI-ready polling on the terminal buffer, then message + Enter send) is cohesive and aligns with the PR goals. The wait_for_cli_ready loop is conservative (2.5s cap, buffer-validity checks, and timeout fallback), and the selection mapping correctly normalizes start_line/end_line and avoids sending anything on empty/cancelled input.


196-202: which-key integration for visual selection is consistent

Registering the selection keymap under mode = 'x' with the same config.keymaps.selection.ask binding and description keeps which-key metadata in sync with the actual mapping and mirrors the existing normal/terminal registrations.

- Add tmux.lua module for tmux pane detection and text sending
- Use ps + awk instead of pgrep -P for macOS compatibility
- Prioritize same-session panes before searching all sessions
- Add tmux config options (enable, prefer_tmux) to config.lua
- Update keymaps.lua to support tmux pane sending for visual selection
- Add send_with_enter() and is_available() helper functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@dukjjang
Copy link
Author

Added: Tmux Integration Support

This commit adds the ability to send text to Claude Code running in a separate tmux pane, which is useful when you prefer running Claude Code in a dedicated tmux window/pane rather than inside nvim.

Changes

New file: lua/claude-code/tmux.lua

  • find_claude_pane(): Finds tmux panes running Claude Code
  • send_text(): Sends text to a tmux pane
  • send_text_with_enter(): Sends text followed by Enter
  • Uses ps + awk instead of pgrep -P for better macOS compatibility
  • Prioritizes same-session panes before searching all sessions

Updated: lua/claude-code/config.lua

  • Added tmux configuration section:
    • enable: Enable/disable tmux integration (default: true)
    • prefer_tmux: Prefer tmux pane over nvim terminal when both exist (default: true)

Updated: lua/claude-code/init.lua

  • send() now checks for tmux panes first if enabled
  • Added send_with_enter() helper function
  • Added is_available() to check if Claude Code is running (returns source: "tmux" or "nvim")

Updated: lua/claude-code/keymaps.lua

  • Visual selection (<leader>cs) now supports sending to tmux panes

Usage

require("claude-code").setup({
  tmux = {
    enable = true,      -- Enable tmux integration
    prefer_tmux = true, -- Prefer tmux pane over nvim terminal
  },
})

When you use <leader>cs in visual mode, the plugin will:

  1. First look for Claude Code in the same tmux session
  2. Fall back to searching all tmux sessions
  3. Fall back to nvim terminal if no tmux pane found

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
lua/claude-code/init.lua (2)

181-189: open() return value may misrepresent readiness when spawning a new instance.

When there is no current instance, M.open() calls M.toggle() and immediately returns true, but in that case the terminal may not yet be visible or ready for input. For existing instances you instead return the result of terminal.ensure_visible(M, M.config), which is closer to “window is now visible”.

To keep this API less surprising for callers, consider one of:

  • Returning the actual visibility state in all cases (e.g., call terminal.ensure_visible after M.toggle() and return its boolean), or
  • Updating the docstring/README to clarify that, for new instances, true means “startup initiated” rather than “window is now visible”.

This is the same behavioral concern raised in a previous review, now applied to the updated open() implementation.

#!/bin/bash
# Inspect external usages of `open()` to see whether they rely on its return value
rg -n "require\('claude-code'\).*open\(" --type lua -C2 || true

108-136: M.send return contract is still misleading when starting a new nvim terminal.

The docstring says @return boolean success True if text was sent successfully, but when there is no current nvim instance you:

  • Call M.toggle()
  • Schedule terminal.send_text(M, text) via vim.defer_fn
  • Immediately return true

At that point you don’t yet know whether terminal.send_text will succeed (job could fail to start, buffer could close, etc.), so true really means “send scheduled/initiated”, not “text was sent successfully”.

Given this function is now part of the public API and also underpins M.send_with_enter, it would be better to either:

  1. Relax the contract (update the docstring to clarify that true means “send has been queued/attempted”, and callers should not rely on it as a delivery guarantee), or
  2. Change the implementation so that it returns a meaningful success boolean in all code paths (e.g., by funneling through a queue that receives the eventual terminal.send_text result and only then resolves the boolean).

Because this mirrors an earlier review concern around M.send's deferred path, tagging as duplicate.

#!/bin/bash
# Show all call sites that rely on the boolean result of M.send
rg -n "require\('claude-code'\).*send\(" --type lua -C2 || true
🧹 Nitpick comments (3)
lua/claude-code/keymaps.lua (1)

117-183: Visual selection flow looks solid; consider reusing send_with_enter for consistency.

The overall selection path is well thought out: it captures the visual range before vim.ui.input, guards against unsaved buffers, builds a compact “See :” message, and distinguishes tmux vs nvim with a new‑session aware wait_for_cli_ready.

For the nvim‑terminal path, you could simplify and de‑duplicate send logic by using the new claude.send_with_enter(message) instead of manually calling claude.send(message) and then claude.send('\r'), letting the public API own the “append Enter” behavior:

-              wait_for_cli_ready(bufnr, function()
-                local sent = claude.send(message)
-                if sent then
-                  vim.defer_fn(function()
-                    claude.send('\r')
-                  end, 50)
-                end
-              end, is_new_session)
+              wait_for_cli_ready(bufnr, function()
+                local sent = claude.send_with_enter(message)
+                if not sent then
+                  vim.notify('Claude Code: Failed to send selection to Claude terminal', vim.log.levels.ERROR)
+                end
+              end, is_new_session)

Not critical, but it centralizes “send + Enter” behavior behind one API and reduces future drift between call sites.

lua/claude-code/config.lua (1)

68-80: Tmux config validation is solid; consider clarifying how to fully disable tmux.

The new ClaudeCodeTmux type, defaults, and validate_tmux_config wiring all look consistent with how init.lua and tmux.lua use config.tmux.

One minor UX edge case: if a user sets tmux = false in their config, vim.tbl_deep_extend('force', ...) will overwrite the default table with false, and validate_tmux_config will then reject it ('tmux config must be a table'), causing a fallback to the full default config (including tmux.enable = true). That means tmux = false won’t actually disable tmux; it will silently re‑enable it via defaults.

Two low‑effort options:

  • Document that the supported way to disable tmux is tmux = { enable = false }, or
  • Special‑case user_config.tmux == false in parse_config to translate it into { enable = false, prefer_tmux = false } before merging.

Not a blocker, but it will prevent confusing “why is tmux still on?” reports.

Also applies to: 124-128, 339-357, 474-479

lua/claude-code/tmux.lua (1)

15-102: Use tmux pane metadata (#{pane_current_command}) as primary detection, with ps tree walk as fallback.

The suggestion to leverage #{pane_current_command} is sound: this format variable is available in modern tmux versions and generally portable across distributions. However, web search confirms an important caveat: on some systems and shell configurations, pane_current_command may report the shell (e.g., zsh, bash) rather than a child process like claude, making it unreliable as the sole detection method.

The recommended approach—check pane_current_command first, then fall back to the existing ps tree walk if needed—is a pragmatic balance: it avoids unnecessary process introspection on systems where tmux's metadata is accurate, while preserving robustness for edge cases.

Example refinement:

  • Use tmux list-panes -s -F '#{pane_id}:#{pane_current_command}' and filter for 'claude' directly.
  • If no match (e.g., shell reports zsh instead), fall back to the existing ps + PID traversal for that pane.

This reduces overhead in typical setups while maintaining cross-platform compatibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1de1c01 and 585c03e.

📒 Files selected for processing (4)
  • lua/claude-code/config.lua (7 hunks)
  • lua/claude-code/init.lua (2 hunks)
  • lua/claude-code/keymaps.lua (2 hunks)
  • lua/claude-code/tmux.lua (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-02T19:40:53.423Z
Learnt from: CR
Repo: greggh/claude-code.nvim PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-07-02T19:40:53.423Z
Learning: Applies to lua/claude-code*/**/*.lua : Main plugin code should reside in '/lua/claude-code', with subdirectories for CLI integration, UI components, and context management

Applied to files:

  • lua/claude-code/tmux.lua
  • lua/claude-code/init.lua
🔇 Additional comments (1)
lua/claude-code/keymaps.lua (1)

220-225: Which‑key registration for visual selection is correct and consistent.

The which-key.add registration for visual mode (mode = 'x') with the same key and description as the mapping keeps UX consistent with the normal/terminal toggle entries. Looks good.

Comment on lines +107 to +158
vim.keymap.set('x', config.keymaps.selection.ask, function()
-- Capture file info BEFORE vim.ui.input (which exits visual mode)
local filepath = vim.fn.expand('%:p')

-- Check for unsaved buffer
if filepath == '' then
vim.notify('Claude Code: Cannot send selection from unsaved buffer', vim.log.levels.WARN)
return
end

-- Use visual mode marks (current selection)
local start_line = vim.fn.line('v')
local end_line = vim.fn.line('.')
-- Ensure start <= end
if start_line > end_line then
start_line, end_line = end_line, start_line
end

vim.ui.input({ prompt = 'Ask Claude: ' }, function(input)
if input and input ~= '' then
-- Just send file path and line range - Claude Code can read the file
local message = string.format(
'See %s:%d-%d\n\n%s',
filepath,
start_line,
end_line,
input
)
local claude = require('claude-code')
local tmux_mod = require('claude-code.tmux')

-- Check if we should use tmux
local use_tmux = false
local tmux_pane = nil
if claude.config.tmux and claude.config.tmux.enable then
tmux_pane = tmux_mod.find_claude_pane()
if tmux_pane then
if claude.config.tmux.prefer_tmux or not claude.claude_code.current_instance then
use_tmux = true
end
end
end

if use_tmux and tmux_pane then
-- Send directly to tmux pane
local sent = tmux_mod.send_text(tmux_pane, message)
if sent then
vim.defer_fn(function()
tmux_mod.send_text(tmux_pane, '\r')
end, 50)
end
else
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read the tmux.lua file to verify send_text and send_text_with_enter implementations
fd -t f -n "tmux.lua" lua/

Repository: greggh/claude-code.nvim

Length of output: 190


🌐 Web query:

tmux send-keys -l carriage return '\r' Enter key behavior

💡 Result:

Short answer: send-keys -l treats its arguments as literal characters, so send-keys -l '\r' will send a backslash followed by r (two characters) — it will not send a carriage-return/Enter. To send Enter use the key name Enter (no -l) or send the actual newline/carriage-return byte(s) (for example via shell expansion).

Examples:

  • tmux send-keys -t pane Enter # sends the Enter key. [1][3]
  • tmux send-keys -t pane C-m # sends Ctrl‑M which is ASCII CR (often acts like Enter). [3]
  • tmux send-keys -t pane $'\r' # shell expands to a literal CR byte and tmux will send it (don’t use -l). [1][4]
  • tmux send-keys -l '\r' # sends the two characters '' and 'r' (literal mode). [1][3]

Sources: tmux man page (send-keys and -l description) and community examples. [1][3]

References
[1] tmux send-keys description — tmux man page.
[2] (man pages mirror) tmux send-keys -l explanation.
[3] Stack Overflow — examples of send-keys, Enter and -l behavior.


🏁 Script executed:

#!/bin/bash
# Search for tmux.lua file in the repository
find . -name "tmux.lua" -o -name "*tmux*" -type f 2>/dev/null | head -20

Repository: greggh/claude-code.nvim

Length of output: 190


🏁 Script executed:

#!/bin/bash
# Also check if the file exists at the expected path
if [ -f "lua/claude-code/tmux.lua" ]; then
  wc -l "lua/claude-code/tmux.lua"
fi

Repository: greggh/claude-code.nvim

Length of output: 190


The repository clone is experiencing persistent failures in the sandbox. However, the web search has already confirmed the core technical concern: tmux send-keys -l '\r' sends literal backslash-r characters, not the Enter key. This validates the review's primary issue.

Given that:

  1. The tmux behavior is definitively confirmed by documentation
  2. The current code at lines 107-158 uses tmux_mod.send_text(tmux_pane, '\r') which will NOT work as intended
  3. The review correctly identifies this as a functional bug

I can now rewrite the review comment based on this verified finding:


Tmux selection path will not send Enter; send-keys -l '\r' sends literal characters instead.

In the tmux branch, you send the message with tmux_mod.send_text(...) and then attempt to press Enter via tmux_mod.send_text(tmux_pane, '\r'). However, send-keys -l treats its arguments as literal characters, so '\r' will send a backslash followed by the letter 'r'—not a carriage return or Enter key. The Claude prompt will appear in the tmux pane but never execute.

Use send_text_with_enter if available, or ensure the tmux command uses the actual Enter key (via send-keys Enter or a shell-expanded carriage return byte without -l flag):

-            local sent = tmux_mod.send_text(tmux_pane, message)
-            if sent then
-                vim.defer_fn(function()
-                  tmux_mod.send_text(tmux_pane, '\r')
-                end, 50)
-            end
+            local sent = tmux_mod.send_text_with_enter(tmux_pane, message)
+            if not sent then
+              vim.notify('Claude Code: Failed to send selection to tmux pane', vim.log.levels.ERROR)
+            end

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lua/claude-code/keymaps.lua around lines 107-158, the tmux branch calls
tmux_mod.send_text(tmux_pane, '\r') which sends the literal characters backslash
and "r" (not an Enter); replace that call with a function or tmux invocation
that actually sends Enter (e.g., use an existing send_text_with_enter/send_enter
helper on tmux_mod, or change the tmux command to use send-keys Enter /
send-keys without -l so the Enter key is emitted), and ensure the deferred send
uses that real-Enter method instead of the string '\r'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant