Skip to content

Latest commit

 

History

History
194 lines (148 loc) · 6.64 KB

File metadata and controls

194 lines (148 loc) · 6.64 KB

Diff Keybinding Implementation Guide for claudecode.nvim

Overview

This guide explains how diff acceptance/rejection works in claudecode.nvim and provides instructions for implementing keybindings in external plugins to communicate with Claude over MCP.

How Diffs Work in claudecode.nvim

The Flow

  1. Claude calls openDiff tool via MCP with:

    • old_file_path: Original file path
    • new_file_path: Path for naming the new buffer
    • new_file_contents: The proposed changes
    • tab_name: Unique identifier for this diff
  2. claudecode.nvim creates a diff view and blocks (using coroutines) waiting for user action

  3. User accepts or rejects the diff through:

    • Accept: Pressing :w (triggers BufWriteCmd autocmd)
    • Reject: Closing the buffer (triggers BufDelete/BufUnload/BufWipeout autocmds)
  4. claudecode.nvim sends response back to Claude:

    • Accepted: {content: [{type: "text", text: "FILE_SAVED"}, {type: "text", text: "<file_contents>"}]}
    • Rejected: {content: [{type: "text", text: "DIFF_REJECTED"}, {type: "text", text: "<tab_name>"}]}

Key Finding: Local Edit vs MCP Communication

IMPORTANT: When the user accepts a diff (:w), claudecode.nvim does NOT write the file locally. Instead:

  1. The BufWriteCmd autocmd prevents the actual file write (returns true)
  2. claudecode.nvim sends FILE_SAVED response to Claude with the buffer contents
  3. Claude performs the actual file write after receiving the response
  4. claudecode.nvim then reloads the buffer to show Claude's changes

This means you should NOT edit the file locally - only inform Claude via MCP so Claude can handle the edit properly.

Implementation for External Plugins

Step 1: Track Diff State

When a diff buffer is created, claudecode.nvim sets buffer variables:

vim.b[buffer].claudecode_diff_tab_name = "unique_tab_name"
vim.b[buffer].claudecode_diff_new_win = window_id  -- The new diff window
vim.b[buffer].claudecode_diff_target_win = window_id -- The original file window

Step 2: Create Keybindings

-- In your plugin's setup
local function setup_diff_keybindings()
  -- Map Enter to accept diff
  vim.keymap.set('n', '<CR>', function()
    local bufnr = vim.api.nvim_get_current_buf()
    local tab_name = vim.b[bufnr].claudecode_diff_tab_name
    
    if not tab_name then
      return -- Not a diff buffer
    end
    
    -- Get buffer contents for the response
    local content_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
    local final_content = table.concat(content_lines, "\n")
    
    -- Add trailing newline if buffer has one
    if #content_lines > 0 and vim.api.nvim_buf_get_option(bufnr, 'eol') then
      final_content = final_content .. "\n"
    end
    
    -- Send MCP response to Claude
    send_mcp_response({
      content = {
        { type = "text", text = "FILE_SAVED" },
        { type = "text", text = final_content }
      }
    })
    
    -- Close the diff windows (optional - Claude will send close_tab)
    local new_win = vim.b[bufnr].claudecode_diff_new_win
    local target_win = vim.b[bufnr].claudecode_diff_target_win
    
    if new_win and vim.api.nvim_win_is_valid(new_win) then
      vim.api.nvim_win_close(new_win, true)
    end
    if target_win and vim.api.nvim_win_is_valid(target_win) then
      vim.api.nvim_set_current_win(target_win)
      vim.cmd("diffoff")
    end
  end, { buffer = bufnr, desc = "Accept diff changes" })
  
  -- Map Escape to reject diff
  vim.keymap.set('n', '<Esc>', function()
    local bufnr = vim.api.nvim_get_current_buf()
    local tab_name = vim.b[bufnr].claudecode_diff_tab_name
    
    if not tab_name then
      return -- Not a diff buffer
    end
    
    -- Send MCP response to Claude
    send_mcp_response({
      content = {
        { type = "text", text = "DIFF_REJECTED" },
        { type = "text", text = tab_name }
      }
    })
    
    -- Close the diff windows
    local new_win = vim.b[bufnr].claudecode_diff_new_win
    local target_win = vim.b[bufnr].claudecode_diff_target_win
    
    if new_win and vim.api.nvim_win_is_valid(new_win) then
      vim.api.nvim_win_close(new_win, true)
    end
    if target_win and vim.api.nvim_win_is_valid(target_win) then
      vim.api.nvim_set_current_win(target_win)
      vim.cmd("diffoff")
    end
  end, { buffer = bufnr, desc = "Reject diff changes" })
end

-- Set up autocmd to add keybindings when diff buffers are created
vim.api.nvim_create_autocmd("BufEnter", {
  callback = function()
    local bufnr = vim.api.nvim_get_current_buf()
    if vim.b[bufnr].claudecode_diff_tab_name then
      setup_diff_keybindings()
    end
  end
})

Step 3: Send MCP Response

You need to send the response through the existing WebSocket connection to Claude:

function send_mcp_response(response_data)
  -- This depends on how your plugin manages the WebSocket connection
  -- You need to:
  -- 1. Find the pending request ID for the openDiff call
  -- 2. Send a JSON-RPC response with that ID
  
  local response = {
    jsonrpc = "2.0",
    id = get_pending_diff_request_id(), -- You need to track this
    result = response_data
  }
  
  -- Send via your WebSocket implementation
  websocket_send(vim.json.encode(response))
end

Important Notes

  1. DO NOT write the file locally - Claude handles the actual file write after receiving FILE_SAVED
  2. The diff operation is blocking - Claude waits for your response before continuing
  3. Track request IDs - You need to respond with the same ID from the original openDiff request
  4. Buffer cleanup - Claude will send a close_tab tool call after processing the response
  5. Navigation - After accepting, the plugin handles navigating to the changed file location

Alternative: Direct Integration

Instead of reimplementing, you could directly call claudecode.nvim's functions:

-- To accept current diff
require('claudecode.diff').accept_current_diff()

-- To reject current diff  
require('claudecode.diff').deny_current_diff()

These functions handle all the MCP communication internally.

Testing Your Implementation

  1. Start claudecode.nvim server: :ClaudeCodeStart
  2. Ask Claude to make a code change that opens a diff
  3. Test your keybindings:
    • Press Enter → Should accept and Claude writes the file
    • Press Escape → Should reject and close the diff
  4. Verify Claude receives the correct response and continues appropriately

Debugging Tips

  • Check buffer variables: :echo b:claudecode_diff_tab_name
  • Monitor WebSocket messages for proper MCP responses
  • Ensure request IDs match between openDiff call and response
  • Watch for Claude's follow-up close_tab tool call after accepting