This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
diffusion.nvim is a unified Neovim plugin that integrates modern AI coding assistants (Claude Code, OpenCode, and Codex) with first-class diff review and an optional immersive in-editor chat overlay. Claude/Pi use MCP over WebSocket, OpenCode uses HTTP/SSE, and Codex uses codex app-server over stdio JSON-RPC.
Focus: The primary integration target is Claude Code via MCP over WebSocket.
Critical Performance Reference: This codebase has undergone extensive performance analysis documented in DIFFUSION_PERFORMANCE_OPTIMIZATION.md. Key findings show potential for 40-60% performance improvement through fixing double navigation issues, optimizing event scheduling, and implementing async operations. Always reference this document when working on performance-critical code.
- Run tests:
cd diffusion-v1 && lua tests/run_tests.lua- Execute complete test suite - Single test:
lua tests/unit/specific_test.lua- Run individual test files - Component tests: Run tests in
tests/component/,tests/integration/,tests/unit/ - Format Lua: Use stylua or your editor's Lua formatter
- Debug logging: Check
~/.local/share/nvim/diffusion.log(user's config setslogging.file = vim.fn.stdpath("data") .. "/diffusion.log") - Performance debugging: Enable
logging.level = "DEBUG"for timing data
- Setup and start:
require('diffusion').setup(config)then:DiffusionStart - Send message:
:CC <message>or:Diffusion <message> - Send selection:
:DiffusionSendSelectionor<leader>ds - Check status:
:DiffusionStatus - Test diff system:
:DiffusionTestDiff
- System status:
:lua print(vim.inspect(require('diffusion').status())) - Claude handler status:
:lua print(vim.inspect(require('diffusion.protocol.claude'):get_status())) - Active diffs:
:lua print(vim.inspect(require('diffusion.diff'):get_active_diffs())) - Force reconnection:
:lua require('diffusion.protocol.claude'):trigger_reconnection()
The plugin follows a provider-agnostic event-driven architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────┐
│ Plugin Entry Layer │ plugin/diffusion.lua
│ (Commands & Keymaps) │
├─────────────────────────────────────────────────────┤
│ Protocol Abstraction │ protocol/init.lua
│ (Claude, OpenCode, Codex) │ protocol/claude.lua
├─────────────────────────────────────────────────────┤
│ Core Diff System │ diff/manager.lua
│ (Provider-Agnostic Logic & State) │ diff/display/, diff/navigation.lua
├─────────────────────────────────────────────────────┤
│ WebSocket MCP Server & Tools │ server/, tools/
├─────────────────────────────────────────────────────┤
│ Event System & Utilities │ utils/events.lua, utils/logger.lua
└─────────────────────────────────────────────────────┘
- Provider Agnosticism: Core functionality in
diff/manager.luais provider-independent - Event-Driven Communication: Components communicate via pub/sub system in
utils/events.lua - Thin Protocol Adapters: Protocol handlers (like
protocol/claude.lua) only translate between provider formats and core API - Reactive Architecture: UI responds to events rather than direct control flow
- Resource Management: Automatic cleanup and lifecycle management throughout
Essential: Claude Code handles ALL file editing. Diffusion's role is purely reactive UI display:
- 🚀 Claude calls tool →
showDifforopenDiffvia WebSocket MCP - 📋 Diffusion displays → Creates diff UI (
diff/manager.lua:show_diff()) - 🛑 Claude calls close →
close_tabtool (regardless of accept/reject) - 🔚 Diffusion dismisses & navigates → Closes UI and navigates to file (
diff/manager.lua:dismiss_diff())
When a diff is accepted, the file buffer is populated from in-memory new_content
instantly in the diff:user_response handler — BEFORE dismiss_diff runs.
This is the optimistic navigation call in manager.lua (external ops path):
-- In diff:user_response handler, external ops, accepted branch:
self._navigation:navigate_to_file(
diff_entry.file_path,
diff_entry.navigation_line,
diff_entry.changed_lines,
nil,
diff_entry.new_content -- from memory, not disk
)This call must NEVER be removed. Without it, the buffer only updates after
dismiss_diff completes its cleanup, causing a visible delay where old content
flashes before new content appears. This has been accidentally removed multiple
times during debugging and each time caused regressions.
See docs/optimistic-buffer-system.md for full documentation.
Key constraints:
- The optimistic call handles buffer content only — NOT positioning or highlighting
dismiss_diff→diff:dismissed→ navigation handles positioning and highlighting- Both passes use the same
new_content— the second pass is idempotent - The IDE must NOT write to disk — Claude Code monitors file hashes and will break
From DIFFUSION_PERFORMANCE_OPTIMIZATION.md analysis:
- Step 1-2: Navigation data collection (15-25ms) happens AFTER buffer creation to avoid blocking UI
- Step 3: Buffer deletion is instant via
vim.defer_fn(..., 0)to escape fast event context - Step 4: Navigation uses event-driven approach to prevent double visual jumps
- Primary role: Provider-agnostic diff lifecycle management
- Key methods:
show_diff(),dismiss_diff(), navigation data collection - Events emitted:
diff:created,diff:dismissedwith navigation data - Performance note: Contains expensive git diff parsing - see optimization guide
- Primary role: Handle file navigation after diff dismissal
- Events listened:
diff:dismissedfor Claude protocol - Critical issue: Double navigation visual jump (documented in optimization guide)
- Fix location: Lines 62-72 need atomic cursor positioning
- Primary role: Translate MCP tool calls to core diff operations
- Critical tools:
showDiff,close_tab,dismissDiff - Performance note: Heavy use of
vim.defer_fn(..., 0)for fast event context - Architecture violation: Should delegate ALL logic to core, no provider-specific handling
- Primary role: Pub/sub communication between components
emit()is synchronous (no vim.schedule overhead); useemit_async()only from libuv callbacks- Listener cleanup is MANDATORY — see rule below
Every events:on() call MUST have a corresponding cleanup path. Orphaned listeners accumulate across start/stop cycles and cause memory leaks + duplicate handler execution.
-- Store the unsubscribe function returned by events:on()
self._event_unsubs = {}
function MyModule:_setup_events()
-- Clear any orphaned listeners from prior cycles BEFORE registering
self:_cleanup_events()
self._event_unsubs[#self._event_unsubs + 1] = self._events:on("my:event", function(data)
-- handler logic
end)
end
function MyModule:_cleanup_events()
for _, unsub in ipairs(self._event_unsubs) do
unsub()
end
self._event_unsubs = {}
end
-- MUST call _cleanup_events() in stop()/destroy()/cleanup()
function MyModule:stop()
self:_cleanup_events()
end-- BAD: listener registered with no cleanup path — WILL leak
events:on("some:event", function(data) ... end)Why this matters: Each DiffusionStop + DiffusionStart cycle re-runs setup. Without cleanup, listeners double on every cycle. After 5 restarts, every event fires 5 duplicate handlers.
-- In diff/manager.lua
function DiffManager:show_diff(data)
-- Provider-independent logic here
self._events:emit("diff:created", diff_entry)
end
-- In protocol/claude.lua
protocol.on_tool_call("showDiff", function(data)
-- Just delegate to core
self._diff_manager:show_diff(data)
end)-- BAD: Don't do this in manager.lua
if data.protocol == "claude" then
-- Claude-specific handling
elseif data.protocol == "opencode" then
-- OpenCode-specific handling
endBased on DIFFUSION_PERFORMANCE_OPTIMIZATION.md analysis:
- Double Navigation (
diff/navigation.lua:62-72) - 5min fix, eliminates visual jump - Git Diff Parsing (
diff/manager.lua:675-790) - 15-25ms blocking operation - Event Scheduling (
utils/events.lua:84) - 100-250ms cumulative overhead - Memory Leaks - Fixed: event listeners now use unsub pattern (see Mandatory Event Listener Lifecycle Rule above)
- Immediate (1-2 days): Fix double navigation, cache git parsing, conditional event scheduling
- Medium-term (1 week): Async I/O, buffer pooling, performance monitoring
- Long-term (1 month): Complete provider abstraction, advanced caching, telemetry
- Entry point:
server/init.lua- Port discovery and lifecycle - WebSocket implementation:
server/websocket_server.lua- RFC 6455 compliant - Authentication:
server/handshake.lua- Token-based validation - Performance note: Already optimized with XOR implementations and string buffer pooling
- Registry:
tools/registry.lua- MCP tool registration and discovery - Execution:
tools/executor.lua- Tool call routing and validation - Schema validation:
tools/validator.lua- Parameter validation
tests/
├── unit/ # Component unit tests
├── integration/ # Cross-component integration tests
├── component/ # Individual component behavior tests
├── mocks/ # Test mocks and fixtures
└── fixtures/ # Test data and configurations
- Full suite:
lua tests/run_tests.lua - Specific tests:
lua tests/unit/diff/manager_test.lua - Mock objects: Available in
tests/mocks/for isolated testing
- User config: Passed to
setup() - Service-specific: Per-protocol configurations
- Runtime config: Dynamic configuration updates
- Validation: Schema validation with helpful error messages
require('diffusion').setup({
services = {
claude = {
enabled = true,
auto_reconnection = true,
diff = {
force_blocking = true, -- Required for Claude
context_lines = 3, -- Performance optimization
concurrent_limit = 3 -- Resource management
}
}
},
logging = {
level = "DEBUG" -- For performance analysis
}
})- Slow diff creation: Check git diff parsing in manager.lua lines 675-790
- Double navigation jumps: Issue in navigation.lua lines 62-72
- Memory growth: Event listener cleanup in events.lua
- Connection issues: WebSocket handshake and token validation
- Location:
~/.local/share/nvim/diffusion.log(user's config overrides default) - Performance data: Enable DEBUG level for timing information
- Event tracing: Look for event emission/handling sequences
- Connection flow: Track WebSocket handshake and MCP tool calls
- Read performance guide first: Always check
DIFFUSION_PERFORMANCE_OPTIMIZATION.mdfor relevant optimizations - Core vs protocol logic: Keep business logic in
diff/manager.lua, not protocol handlers - Event-driven communication: Use events for component communication
- Test with real Claude: Use
:CC <message>to test integration - Monitor performance: Check DEBUG logs for timing regressions
- Add new tool:
tools/registry.lua - Modify diff behavior:
diff/manager.lua - Fix navigation issues:
diff/navigation.lua - Protocol-specific logic:
protocol/claude.lua(keep minimal) - Event handling:
utils/events.lua - Performance optimization: Reference optimization guide sections
- Local-only binding: Server bound to
127.0.0.1(server/init.lua) - Token-based auth: UUID4 tokens in lock files (
utils/crypto.lua) - File validation: Path validation in tool handlers (
utils/file.lua) - Process isolation: Different tokens per workspace
- Token validation:
server/handshake.lua:validate_auth() - Crypto functions:
utils/crypto.lua - File safety:
utils/file.lua:validate_file_path()
- Connection stats: Each protocol handler tracks usage
- Resource monitoring: Active diffs, connections, buffers
- Event statistics: Event emission and handling counts
- Timing data: Available in DEBUG logs
- Diff creation: <10ms (currently 20-35ms)
- Memory baseline: <10MB (currently 15-25MB)
- Event processing: <100ms total (currently 100-250ms)
- Navigation: Zero visual jumps (currently has double jump)
When working on this codebase:
- Always reference
DIFFUSION_PERFORMANCE_OPTIMIZATION.mdfor performance context - Maintain provider-agnostic architecture - keep core logic in
diff/manager.lua - Use event system for communication - avoid direct component coupling
- Test with real Claude integration - use
:CCcommands for validation - Monitor performance impact - enable DEBUG logging and watch for regressions
- Follow the 4-step diff flow - understand Claude's reactive nature
- Be mindful of fast event context - use
vim.defer_fn(..., 0)when needed