I Built a Claude Code Context Modal Inside of Neovim

Sometimes I just wish I had AI right at this moment, right with this text
July 26, 2025

Kai Neovim AI Assistant

Kai: AI-powered coding in Neovim (click for full size)

I've been using AI to help with coding for a while now, but going back and forth between my code and AI was getting tedious—even with a highly optimized setup. So I integrated my Digital Assistant, Kai, directly into Neovim.

The Problem

When working with AI for code editing, you typically want one of these actions:

  • Replace selected code with an improved version
  • Insert new code based on context
  • Display information without modifying anything

Most AI integrations make you tell them exactly what to do. Kai somewhat figures it out from how you ask.

How It Works

This entire thing is based on a capability in Claude Code that is massively underdiscussed. I just think of it as command-line mode.

bash
claude -p
  • You can pipe into this thing
  • You could give it a string afterwards and it will just go and execute
  • You can even control how it uses different contexts and such.
bash
claude -p "What's the weather like in San Francisco right now?"

Anyway, that's what's going on under the hood. It's this command-line version of Claude Code that we're actually calling with this plug-in.

Structure

The plugin has two main pieces:

  1. Lua plugin (kai-neovim.lua) - Handles Neovim integration, visual selections, and buffer management
  2. Shell script (kai-neovim.sh) - Processes context and communicates with the AI backend

Smart Context Handling

Kai Progress Window

Kai's enhanced progress window showing detailed processing phases

The plugin always sends the entire buffer as context, but intelligently focuses on:

  • Selected text when you're in visual mode
  • Cursor position when you're in normal mode

This approach provides comprehensive context while enabling precise, targeted modifications based on your current selection or cursor position.

Intelligent Action Detection

The plugin lets you basically tell it anything, and it tries to work it out. Here are some examples.

  • "Replace with X" → Replaces selection
  • "Add a comment explaining" → Inserts after selection
  • "What does this do?" → Shows analysis in a popup
  • "Insert an appropriate image here." → Creates a custom image and inserts it at that location
  • "Fix the error" → Replaces with corrected code

This is just completely insane. We can just send arbitrary things and have it kind of figure it out. Basically, command-line interaction with AI. And within your text editor.

The Code

This is the basic code for it, but keep in mind it's a work in progress. It might be total garbage. And yes, I had Kai help me build it for sure.

Main Plugin Code

Save this as ~/.config/nvim/lua/kai-neovim.lua:

lua
local M = {}

-- Function to get visual selection
local function get_visual_selection()
  -- Get the visual selection marks
  local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<"))
  local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>"))
  
  -- Get the lines
  local lines = vim.api.nvim_buf_get_lines(0, start_row - 1, end_row, false)
  
  if #lines == 0 then
    return ""
  end
  
  -- Handle single line selection
  if #lines == 1 then
    lines[1] = string.sub(lines[1], start_col, end_col)
  else
    -- Multi-line selection
    lines[1] = string.sub(lines[1], start_col)
    if end_col > 0 then
      lines[#lines] = string.sub(lines[#lines], 1, end_col)
    end
  end
  
  return table.concat(lines, "\n")
end

-- Function to escape special characters for shell
local function shell_escape(str)
  return "'" .. str:gsub("'", "'\"'\"'") .. "'"
end

-- Main function to handle Kai Neovim integration
function M.kai_enhance()
  -- Set up subtle blue highlight for the input prompt
  vim.cmd('highlight KaiPrompt guifg=#e0e0e0 guibg=#1a1a2e')
  
  -- Get the prompt from user with custom highlighting
  vim.cmd('echohl KaiPrompt')
  local prompt = vim.fn.input("🤖 Kai: ")
  vim.cmd('echohl None')
  
  if prompt == "" then
    print("No instruction provided.")
    return
  end
  
  -- Check if we're in visual mode
  local mode = vim.fn.mode()
  local is_visual = mode == 'v' or mode == 'V' or mode == ''
  
  -- Get selection if in visual mode, empty string otherwise
  local selection = ""
  if is_visual then
    selection = get_visual_selection()
  end
  
  -- Get current file path
  local filepath = vim.fn.expand('%:p')
  
  -- Get cursor position
  local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0))
  
  -- Get entire buffer content
  local buffer_content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), "\n")
  
  -- Create a temporary file for the context
  local context_file = os.tmpname()
  local f = io.open(context_file, "w")
  f:write("CURRENT FILE: " .. filepath .. "\n\n")
  
  -- Always send the entire buffer
  f:write("FULL BUFFER CONTENT:\n" .. buffer_content .. "\n\n")
  
  -- Add cursor position
  f:write("CURSOR POSITION: Line " .. cursor_row .. ", Column " .. cursor_col .. "\n\n")
  
  if is_visual then
    -- Include selection information when text is selected
    local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<"))
    local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>"))
    
    f:write("SELECTED TEXT (Lines " .. start_row .. "-" .. end_row .. "):\n" .. selection .. "\n\n")
    f:write("MODE: User has selected specific text. Focus on this selection within the context of the entire buffer.\n\n")
  else
    -- When no selection, note cursor position
    f:write("MODE: No selection. User's cursor is at line " .. cursor_row .. ". Make targeted changes based on cursor location unless instructed otherwise.\n\n")
  end
  
  f:write("INSTRUCTION: " .. prompt .. "\n")
  f:close()
  
  -- Call Kai script
  local cmd = string.format(
    "~/.config/nvim/scripts/kai-neovim.sh %s %s",
    shell_escape(context_file),
    shell_escape(prompt)
  )
  
  -- Create progress notification (simplified for blog post)
  print("🤖 Processing with Kai...")
  
  -- Execute command
  local output = vim.fn.system(cmd)
  
  -- Clean up temp file
  os.remove(context_file)
  
  -- Parse the action and content from the response
  local lines = vim.split(output, '\n', { plain = true })
  local action = lines[1]
  local content_lines = {}
  for i = 2, #lines do
    if lines[i] ~= "" or i < #lines then
      table.insert(content_lines, lines[i])
    end
  end
  local content = table.concat(content_lines, '\n')
  
  -- Remove any trailing newline
  content = content:gsub('\n$', '')
  
  -- Handle different actions
  if action == "[ACTION:DISPLAY]" then
    -- Create a floating window to display the analysis
    local display_buf = vim.api.nvim_create_buf(false, true)
    local display_lines = vim.split(content, '\n', { plain = true })
    
    -- Calculate window dimensions
    local width = math.min(80, vim.o.columns - 10)
    local height = math.min(#display_lines + 2, vim.o.lines - 10)
    
    -- Set buffer content
    vim.api.nvim_buf_set_lines(display_buf, 0, -1, false, display_lines)
    
    -- Create floating window
    local display_win = vim.api.nvim_open_win(display_buf, true, {
      relative = 'editor',
      width = width,
      height = height,
      col = math.floor((vim.o.columns - width) / 2),
      row = math.floor((vim.o.lines - height) / 2),
      style = 'minimal',
      border = 'rounded',
      title = ' Kai Analysis ',
      title_pos = 'center',
    })
    
    -- Set up keymaps to close the window
    local close_keys = {'<Esc>', 'q', '<CR>'}
    for _, key in ipairs(close_keys) do
      vim.api.nvim_buf_set_keymap(display_buf, 'n', key, 
        ':lua vim.api.nvim_win_close(' .. display_win .. ', true)<CR>', 
        { noremap = true, silent = true })
    end
    
    print("Kai analysis complete! Press <Esc>, q, or <Enter> to close.")
    return
  end
  
  -- Perform the appropriate action based on the marker
  if is_visual then
    if action == "[ACTION:REPLACE]" then
      -- Replace the selection
      local save_reg = vim.fn.getreg('"')
      local save_regtype = vim.fn.getregtype('"')
      
      vim.fn.setreg('"', content, mode == 'V' and 'V' or 'v')
      vim.cmd('normal! gv"_d')  -- Delete selection without affecting registers
      vim.cmd('normal! P')      -- Paste before cursor
      
      vim.fn.setreg('"', save_reg, save_regtype)
      
    elseif action == "[ACTION:INSERT_AFTER]" then
      -- Insert after the selection
      vim.cmd('normal! gv')  -- Reselect
      vim.cmd('normal! o')   -- Go to end of selection
      vim.cmd('normal! ')    -- Exit visual mode
      
      -- Insert a newline and the content
      local row, col = unpack(vim.api.nvim_win_get_cursor(0))
      local content_lines_new = vim.split(content, '\n', { plain = true })
      
      -- Insert empty line first, then content
      vim.api.nvim_buf_set_lines(0, row, row, false, {""})
      vim.api.nvim_buf_set_lines(0, row + 1, row + 1, false, content_lines_new)
    end
  else
    -- Normal mode - insert at cursor position
    local content_lines_new = vim.split(content, '\n', { plain = true })
    local row, col = unpack(vim.api.nvim_win_get_cursor(0))
    
    -- Insert the lines at cursor position
    vim.api.nvim_buf_set_text(0, row - 1, col, row - 1, col, content_lines_new)
  end
  
  print("Kai enhancement complete!")
end

-- Set up the keymap
function M.setup()
  -- Visual mode mapping
  vim.keymap.set('v', '<leader>ai', M.kai_enhance,
    { noremap = true, silent = true, desc = "Enhance with Kai (intelligent action)" })
  
  -- Normal mode mapping (insert at cursor)
  vim.keymap.set('n', '<leader>ai', M.kai_enhance, 
    { noremap = true, silent = true, desc = "Insert Kai text at cursor" })
end

return M

Shell Script

Create ~/.config/nvim/scripts/kai-neovim.sh:

bash
#!/bin/bash

# Kai Neovim Enhancement Script with Intelligent Action Detection
# Usage: kai-neovim.sh <context_file> <prompt>

CONTEXT_FILE="$1"
PROMPT="$2"

# Check if claude CLI is available (we use it to communicate with Kai)
if ! command -v claude &> /dev/null; then
    echo "Error: Claude CLI not found. Please install it first."
    exit 1
fi

# Read the CLAUDE.md files for additional context (project-specific rules for Kai)
GLOBAL_CLAUDE_MD=""
LOCAL_CLAUDE_MD=""

if [ -f "$HOME/.claude/CLAUDE.md" ]; then
    GLOBAL_CLAUDE_MD=$(cat "$HOME/.claude/CLAUDE.md")
fi

# Find the nearest CLAUDE.md in the project
CURRENT_DIR=$(pwd)
while [ "$CURRENT_DIR" != "/" ]; do
    if [ -f "$CURRENT_DIR/CLAUDE.md" ]; then
        LOCAL_CLAUDE_MD=$(cat "$CURRENT_DIR/CLAUDE.md")
        break
    fi
    CURRENT_DIR=$(dirname "$CURRENT_DIR")
done

# Regular text enhancement request - let Kai determine the action
FULL_PROMPT="You are Kai, an AI assistant integrated into Neovim. 

CRITICAL CONTEXT FROM CLAUDE.md FILES (FOLLOW THESE RULES EXACTLY):
==================================================
GLOBAL CLAUDE.md:
$GLOBAL_CLAUDE_MD

PROJECT CLAUDE.md:
$LOCAL_CLAUDE_MD
==================================================

CURRENT EDITING CONTEXT:
$(cat "$CONTEXT_FILE")

CRITICAL: INTELLIGENT ACTION DETECTION
You must analyze the user's instruction to determine what they want:

1. If they say things like \"replace with\", \"change to\", \"rewrite as\", \"make this\", \"convert to\" → REPLACE the selected text
2. If they say things like \"write something like this\", \"create a note about\", \"add after\", \"insert\" → INSERT new content (don't replace)
3. If they say things like \"improve\", \"enhance\", \"fix\", \"correct\" → REPLACE with improved version
4. If they say things like \"explain this\", \"what is this\", \"analyze\", \"tell me about\", \"show me\", \"list\", \"count\", \"find\" → DISPLAY information (don't modify file)

IMPORTANT: When working with selected text, focus on that specific text within the context of the entire buffer. When working without selection, make targeted changes at the cursor location.

RESPONSE FORMAT:
You must start your response with ONE of these action markers on its own line:
[ACTION:REPLACE]
[ACTION:INSERT_AFTER]
[ACTION:INSERT_BEFORE]
[ACTION:DISPLAY]

Then on the next line, provide the content:
- For REPLACE/INSERT actions: provide ONLY the text to insert (no explanations)
- For DISPLAY actions: provide the analysis/information to show the user

IMPORTANT INSTRUCTIONS:
- First line must be the action marker
- Follow ALL formatting rules from CLAUDE.md
- Maintain the code style and conventions of the file
- Consider the context when generating content
- You are Kai, the AI assistant integrated into Neovim

User instruction: $PROMPT"

# Get the response with action marker
RESPONSE=$(echo "$FULL_PROMPT" | claude -p)  # Using claude CLI to communicate with Kai

# Output the response
echo "$RESPONSE"

# Exit with the command's exit code
exit $?

Don't forget to make the script executable:

bash
chmod +x ~/.config/nvim/scripts/kai-neovim.sh

Summary

All right, that should get you started with the structure for your own implementation.

And now you can talk directly to your AI from within Neovim!

Notes

  1. ⚠️ Caveat Aedificator (Builder Beware): When you start asking AI to generate shell commands or code that executes system commands, things can go wonky pretty quick. Always review AI-generated code before running it, especially if it involves shell execution. This plugin runs commands locally on your machine, so treat it with the same caution you'd give any code execution tool.
  2. AIL 2 - I (Daniel's DA, Kai) helped with structuring and formatting this blog post, putting this content at AIL Level 2. Read more about AIL
  3. From Kai, inserted using this actual plugin: "The official Neovim term for what I'm creating is a "floating window" (using vim.api.nvim_open_win()), which is Neovim's implementation of modal-like popup windows that float above the main editor interface."
Thank you for reading...