Document Version: 1.0 Created: 2025-10-22 Status: Active Refactoring Guide Last Commit: 811e953 (refactor(claude): partial migration to inline protocol implementation)
The OpenCode provider has been successfully integrated into diffusion.nvim, providing:
- HTTP client connectivity to OpenCode's API
- Server-Sent Events (SSE) diff streaming
- Real-time edit proposals and acceptance workflow
- Automatic workspace detection and connection
- Provider switching between Claude and OpenCode
Total Lines of Code: 4,533 lines (6 core files analyzed) Modified Files: 8 files in current working tree Architecture Health: 🟨 Yellow - Functional but needs refactoring Provider Agnosticism: 🔴 Red - Significant violations present
Key Metrics:
protocol/claude.lua: 1,147 lines (target: ~350 lines, -797 lines needed)client/opencode_client.lua: 1,075 lines (target: ~600 lines, -475 lines needed)protocol/opencode.lua: 476 lines (acceptable range)plugin/functions.lua: 757 lines (needs abstraction extraction)config.lua: 719 lines (needs business logic separation)
This document provides a systematic, phased approach to refactoring the codebase to achieve:
- ✅ Full provider-agnostic architecture
- ✅ Standardized handler interfaces
- ✅ ~1,200 lines of code reduction
- ✅ Elimination of architectural violations
- ✅ Maintainable, testable, extensible design
plugin/functions.lua | 212 changes (+142/-70)
Refactoring Headers Added (8 files):
plugin/functions.lua- Commands layer with architectural violationslua/diffusion/protocol/init.lua- Protocol routing layerlua/diffusion/protocol/claude.lua- Claude MCP handlerlua/diffusion/protocol/opencode.lua- OpenCode HTTP handlerlua/diffusion/init.lua- Core initializationlua/diffusion/ui/init.lua- UI and selection layerlua/diffusion/config.lua- Configuration managementlua/diffusion/client/opencode_client.lua- HTTP client transport
-
Missing
send_at_mention()Abstraction- Location:
protocol/init.lua - Impact: DiffusionSend command bypasses protocol layer
- Violation: Commands directly access handler internals
- Files affected:
plugin/functions.lua(lines 128-143) - Estimated fix time: 2 hours
- Location:
-
DiffusionSend Directly Accesses Claude Handler
- Location:
plugin/functions.lua:129 - Code:
claude_handler._server:broadcast_message() - Impact: Breaks provider agnosticism, OpenCode can't use command
- Estimated fix time: 1 hour (depends on #1)
- Location:
-
Handler Interface Not Standardized
- Missing methods:
send_at_mention()in both handlers - No interface validation on initialization
- Impact: Runtime errors, inconsistent behavior
- Estimated fix time: 4 hours
- Missing methods:
-
Visual Selection Extraction Duplicated
- Location:
plugin/functions.lua:113-126,ui/init.lua:41-222 - Should be:
utils/selection.lua:get_visual_marks() - Impact: Duplication, inconsistent behavior
- Lines to save: ~80 lines
- Estimated fix time: 3 hours
- Location:
-
Business Logic in Claude Handler
- Location:
protocol/claude.lua(1,147 lines, target: 350) - Tool implementations inline (should be in ToolService)
- Diff management logic (should delegate to DiffManager)
- Tmux operations (should be in TmuxService)
- Impact: Massive file, hard to maintain, duplication risk
- Lines to extract: ~797 lines
- Estimated fix time: 2-3 days
- Location:
-
Business Logic in OpenCode Client
- Location:
client/opencode_client.lua(1,075 lines, target: 600) - Diff dismissal logic (lines 642-694)
- Diff tracking in
_pending_diffs - Navigation after edits
- Impact: Transport layer doing business logic
- Lines to extract: ~475 lines
- Estimated fix time: 1-2 days
- Location:
-
Config Module Has Auto-Mode Logic
- Location:
config.lua:296-336 - Code modifies handler
enabledflags - Should be: Protocol layer interprets "auto" mode
- Impact: Separation of concerns violation
- Estimated fix time: 2 hours
- Location:
-
Selection Tracking Calls Handlers Directly
- Location:
ui/init.lua - Should emit events, handlers subscribe optionally
- Impact: Tight coupling, not provider-agnostic
- Estimated fix time: 2 hours
- Location:
-
No Schema Validation in Config
- Location:
config.lua - Only basic nil checks
- Should have: JSON-schema style validation
- Estimated fix time: 4 hours
- Location:
-
Patch Parsing Duplicated
- Location:
client/opencode_client.lua - Should be: Shared diff utility
- Impact: Minor duplication
- Estimated fix time: 1 hour
- Location:
Problem Statement:
The :DiffusionSend command (used for <leader>s keybinding) directly accesses Claude handler internals, completely bypassing the protocol abstraction layer. This violates the core architectural principle that commands should never know which provider is active.
Current Code Flow (BROKEN):
User presses <leader>s
→ DiffusionSend command
→ Directly accesses: diffusion._protocol._handlers.claude
→ Calls: claude_handler._server:broadcast_message()
→ Sends MCP "at_mentioned" message
Correct Code Flow (TARGET):
User presses <leader>s
→ DiffusionSend command
→ Calls: diffusion._protocol:send_at_mention(file, start, end)
→ Protocol gets active handler
→ Delegates to: handler:send_at_mention(file, start, end)
→ Claude: Sends MCP at_mentioned via WebSocket
→ OpenCode: Sends @file:line-line via HTTP
Files to Modify:
-
lua/diffusion/protocol/init.lua- Add protocol methodfunction Protocol:send_at_mention(file_path, start_line, end_line) local handler = self:_get_active_handler() if not handler then return false, "No active service" end if not handler.send_at_mention then return false, "Handler does not support at-mentions" end return handler:send_at_mention(file_path, start_line, end_line) end
-
lua/diffusion/protocol/claude.lua- Implement handler methodfunction ClaudeHandler:send_at_mention(file_path, start_line, end_line) if not self._server or not self._server:has_clients() then return false, "No clients connected" end local success = self._server:broadcast_message({ jsonrpc = "2.0", method = "at_mentioned", params = { filePath = file_path, lineStart = start_line - 1, -- 0-based for Claude lineEnd = end_line - 1, } }) if success and self._config.focus_after_send and vim.env.TMUX then vim.fn.system("tmux select-pane -t +1 2>/dev/null") end return success, success and nil or "Broadcast failed" end
-
lua/diffusion/protocol/opencode.lua- Implement handler methodfunction OpenCodeHandler:send_at_mention(file_path, start_line, end_line) if not self._client or not self._client:is_connected() then return false, "Not connected to OpenCode" end local ref = self._client:format_file_reference( file_path, start_line, end_line ) local success, err = self._client:send_prompt(ref, false) if success then self._stats.prompts_sent = self._stats.prompts_sent + 1 end return success, err end
-
plugin/functions.lua- Update command to use protocol-- BEFORE (lines 113-150): local mark_start = vim.fn.getpos("'<")[2] local mark_end = vim.fn.getpos("'>")[2] -- ... validation ... local claude_handler = diffusion._protocol._handlers.claude -- ❌ WRONG local success = claude_handler._server:broadcast_message(...) -- ❌ WRONG -- AFTER: local selection = require('diffusion.utils.selection').get_visual_marks() if not selection then vim.notify("No visual selection found", vim.log.levels.ERROR) return end local success, err = diffusion._protocol:send_at_mention( selection.file_path, selection.start_line, selection.end_line ) if not success then vim.notify("Failed to send: " .. (err or "unknown error"), vim.log.levels.ERROR) end
Dependencies:
- Requires Issue #4 (selection utility extraction) to be completed first
- Blocks: All provider-agnostic command implementations
Estimated Effort: 2-3 hours Impact: HIGH - Enables provider-agnostic commands Risk: LOW - Straightforward delegation pattern
Problem Statement:
Visual selection extraction logic is duplicated between plugin/functions.lua and ui/init.lua, with slightly different implementations. This violates DRY and creates maintenance burden.
Current Duplication:
plugin/functions.lua:113-126- Gets marks for DiffusionSendui/init.lua:41-222- Complex selection extraction with mode handling- Different error handling, different data structures returned
Target Architecture:
Create lua/diffusion/utils/selection.lua with a single, well-tested implementation:
-- lua/diffusion/utils/selection.lua
local M = {}
-- Get visual selection marks
-- Returns: {file_path, start_line, end_line, text, selection_type} or nil
function M.get_visual_marks()
-- Get marks (persist after visual mode exits)
local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
-- Validate marks
if start_pos[2] == 0 or end_pos[2] == 0 then
return nil
end
-- Get file path
local file_path = vim.api.nvim_buf_get_name(0)
if not file_path or file_path == "" then
return nil
end
-- Extract text using marks
local start_line = start_pos[2]
local end_line = end_pos[2]
local start_col = math.max(0, start_pos[3] - 1) -- Convert to 0-based
local end_col = math.max(0, end_pos[3] - 1)
local lines = vim.api.nvim_buf_get_text(
0, start_line - 1, start_col, end_line - 1, end_col, {}
)
local text = table.concat(lines, '\n')
if text == "" then
return nil
end
return {
file_path = file_path,
start_line = start_line,
end_line = end_line,
start_col = start_pos[3], -- Keep 1-based for consistency
end_col = end_pos[3],
text = text,
selection_type = "visual_marks"
}
end
-- Get current selection (visual mode or marks)
function M.get_current_selection()
local mode = vim.fn.mode()
-- If in visual mode, get live selection
if mode == 'v' or mode == 'V' or mode == '\22' then
-- Use marks even in visual mode (more reliable)
return M.get_visual_marks()
end
-- Try marks first
local selection = M.get_visual_marks()
if selection then
return selection
end
-- Fallback to word/line under cursor
return M.get_fallback_selection()
end
-- Get fallback (word or line under cursor)
function M.get_fallback_selection()
local cursor_pos = vim.api.nvim_win_get_cursor(0)
local line_num = cursor_pos[1]
-- Try word under cursor
local word = vim.fn.expand('<cword>')
if word and word ~= "" then
return {
file_path = vim.api.nvim_buf_get_name(0),
start_line = line_num,
end_line = line_num,
text = word,
selection_type = "word"
}
end
-- Fallback to current line
local line = vim.api.nvim_get_current_line()
return {
file_path = vim.api.nvim_buf_get_name(0),
start_line = line_num,
end_line = line_num,
text = line,
selection_type = "line"
}
end
return MFiles to Modify:
- Create:
lua/diffusion/utils/selection.lua(new file) - Update:
plugin/functions.lua:113-126→ use utility - Update:
ui/init.lua:41-222→ use utility - Add:
tests/unit/utils/selection_test.lua(unit tests)
Code Reduction: ~80 lines Estimated Effort: 3 hours Impact: HIGH - Enables Issue #1, improves consistency Risk: LOW - Pure refactoring, behavior unchanged
Problem Statement:
The Claude handler (protocol/claude.lua) is 1,147 lines long when it should be a thin translation layer of ~350 lines. It contains massive amounts of business logic that should be in core services.
Current File Breakdown:
Lines 1-140: Module setup, imports, initialization (OK)
Lines 141-180: is_available() logic (OK, provider-specific)
Lines 181-300: start/stop/connection lifecycle (OK)
Lines 301-400: send_message, send_selection (OK, will add send_at_mention)
Lines 401-700: ❌ TOOL IMPLEMENTATIONS (should be ToolService)
- openFile tool
- showDiff/openDiff tools
- dismissDiff tool
- close_tab tool
- getCurrentSelection tool
- saveDocument tool
- getDiagnostics tool
Lines 701-900: ❌ DIFF MANAGEMENT (should delegate to DiffManager)
- Diff creation logic
- Diff dismissal logic
- Navigation after diff
Lines 901-1000: ❌ TMUX OPERATIONS (should be TmuxService)
- Pane discovery
- /ide command sending
- Pane focusing
Lines 1001-1100: ❌ SELECTION TRACKING (should be UIService)
- Real-time selection monitoring
- selection_changed MCP messages
Lines 1101-1147: WebSocket message handling (OK, protocol-specific)
Refactoring Strategy:
Phase 1: Extract Tool Service (Lines 401-700)
- Create:
lua/diffusion/services/tool_service.lua - Move all tool implementations to service
- Handler becomes: Receive MCP call → Validate → Delegate to ToolService → Return result
- Estimated reduction: 300 lines
Phase 2: Delegate to DiffManager (Lines 701-900)
- Already exists:
lua/diffusion/diff/manager.lua - Remove diff logic from handler
- Change to: Handler translates MCP params → Calls DiffManager methods → Translates response
- Estimated reduction: 200 lines
Phase 3: Extract Tmux Operations (Lines 901-1000)
- Already exists:
lua/diffusion/services/tmux.lua - Move pane discovery to TmuxService
- Handler just calls service methods
- Estimated reduction: 100 lines
Phase 4: Move Selection Tracking (Lines 1001-1100)
- Already exists:
lua/diffusion/ui/init.luahas tracking - Remove handler-specific tracking
- Change to: UI emits events → Handler subscribes → Sends MCP notifications
- Estimated reduction: 100 lines
Target Handler Size:
- Before: 1,147 lines
- After: ~350 lines
- Reduction: 797 lines (69% reduction)
Estimated Effort: 2-3 days Impact: VERY HIGH - Enables provider agnosticism, reduces duplication Risk: MEDIUM - Requires careful refactoring, extensive testing
Problem Statement:
The OpenCode HTTP client (client/opencode_client.lua) is 1,075 lines when it should be a focused transport layer of ~600 lines. It contains business logic for diff management and navigation.
Current File Breakdown:
Lines 1-150: Module setup, connection discovery (OK)
Lines 151-300: HTTP communication, SSE setup (OK)
Lines 301-450: SSE event parsing (OK)
Lines 451-600: ❌ EDIT DETAILS PARSING (should be utils or DiffManager)
- Diff structure parsing
- Content extraction
- Should be shared utility (Claude may need it too)
Lines 601-700: ❌ DIFF DISMISSAL LOGIC (should be DiffManager)
- _handle_file_edited() manually dismisses diffs
- _find_diff_for_file() duplicates DiffManager
- Should just emit event, let Navigation module handle
Lines 701-850: ❌ DIFF TRACKING (_pending_diffs)
- Tracks diff_id by callID
- Should be in DiffManager
- Single source of truth violation
Lines 851-1075: Process discovery, tmux checks (OK, but could simplify)
Refactoring Strategy:
Phase 1: Simplify Event Handling (Lines 601-700)
- Remove direct diff dismissal
- Change to: Just emit "sse:file_edited" event with file_path
- Let OpenCodeHandler or Navigation module listen and handle dismissal
- Estimated reduction: 100 lines
Phase 2: Remove Diff Tracking (Lines 701-850)
- Delete
_pending_diffstable - DiffManager should track call_id → diff_id relationship
- Client doesn't need to know about diffs
- Estimated reduction: 150 lines
Phase 3: Extract Edit Details Parsing (Lines 451-600)
- Create:
lua/diffusion/utils/diff_parser.lua - Shared utility for parsing diff structures
- Both Claude and OpenCode can use
- Estimated reduction: 150 lines
Phase 4: Simplify Format Functions (Various)
- Move
format_file_reference()to handler or utils - Keep only transport-specific code in client
- Estimated reduction: 75 lines
Target Client Size:
- Before: 1,075 lines
- After: ~600 lines
- Reduction: 475 lines (44% reduction)
Estimated Effort: 1-2 days Impact: HIGH - Cleaner separation, easier to maintain Risk: LOW - Well-isolated changes
Current State: Commands layer with direct handler access
Violations:
- ☐ DiffusionSend (lines 95-154): Directly accesses
claude_handler._server - ☐ DiffusionSend (lines 135-143): Provider-specific MCP message
- ☐ DiffusionSend (lines 113-126): Selection extraction should be utility
- ☐ Missing: Protocol abstraction calls
Refactoring Tasks:
-
Extract Selection Utility [HIGH]
- Lines: 113-126
- Target:
lua/diffusion/utils/selection.lua:get_visual_marks() - Complexity: LOW
- Time: 1 hour
-
Add Protocol Delegation [CRITICAL]
- Lines: 95-154
- Change to:
diffusion._protocol:send_at_mention() - Dependency: Requires protocol method first
- Complexity: LOW
- Time: 30 minutes
-
Remove Direct Handler Access [CRITICAL]
- Lines: 129-133
- Delete:
claude_handler._server:broadcast_message() - Complexity: LOW
- Time: 15 minutes
Expected Result:
- ✅ All commands delegate to protocol layer
- ✅ No direct handler access
- ✅ Selection extraction reusable
Code Reduction: ~30 lines (reuse utility)
Current State: Protocol routing layer, missing send_at_mention
Violations:
- ☐ Missing:
send_at_mention()method - ☐ send_selection() exists but no distinction from at-mention
- ☐ No handler interface validation
Refactoring Tasks:
-
Add send_at_mention() Method [CRITICAL]
- Add new method to Protocol class
- Delegate to active handler
- Handle errors gracefully
- Complexity: LOW
- Time: 1 hour
-
Document Selection vs At-Mention [MEDIUM]
- Add comments explaining distinction
- selection: Continuous tracking, real-time
- at_mention: User action, explicit reference
- Complexity: TRIVIAL
- Time: 15 minutes
-
Add Handler Validation [MEDIUM]
- Validate handlers implement required interface on init
- Required methods: start, stop, is_available, is_connected
- Required methods: send_message, send_selection, send_at_mention
- Complexity: MEDIUM
- Time: 2 hours
Expected Result:
- ✅ Complete protocol abstraction for all operations
- ✅ Clear interface requirements documented
- ✅ Runtime validation prevents incomplete handlers
Code Addition: ~60 lines
Current State: Massive handler with embedded business logic
Violations:
- ☐ Missing:
send_at_mention()method - ☐ File size: 1,147 lines (should be ~350)
- ☐ Tool implementations inline (lines 401-700)
- ☐ Diff management logic (lines 701-900)
- ☐ Tmux operations (lines 901-1000)
- ☐ Selection tracking (lines 1001-1100)
Refactoring Tasks:
-
Add send_at_mention() Method [CRITICAL]
- Insert after send_selection() method
- ~30 lines implementation
- Complexity: LOW
- Time: 30 minutes
-
Extract Tool Implementations [HIGH] ⏰ 2 days
- Create:
lua/diffusion/services/tool_service.lua - Move: Lines 401-700 → ToolService
- Handler becomes thin wrapper
- Complexity: HIGH
- Time: 2 days
- Create:
-
Delegate Diff Operations [HIGH] ⏰ 1 day
- Remove: Lines 701-900
- Use: Existing DiffManager methods
- Handler translates MCP ↔ DiffManager
- Complexity: MEDIUM
- Time: 1 day
-
Extract Tmux Service Calls [MEDIUM] ⏰ 4 hours
- Remove: Lines 901-1000
- Use: Existing TmuxService
- Complexity: LOW
- Time: 4 hours
-
Move Selection Tracking to UI [MEDIUM] ⏰ 3 hours
- Remove: Lines 1001-1100
- Change: UI emits events, handler subscribes
- Complexity: MEDIUM
- Time: 3 hours
Expected Result:
- ✅ Handler is ~350 lines (thin translation layer)
- ✅ All business logic in core services
- ✅ Easier to maintain and test
- ✅ No duplication with OpenCode handler
Code Reduction: 797 lines (69%)
Current State: Reasonably sized, but missing send_at_mention
Violations:
- ☐ Missing:
send_at_mention()method - ☐ Some diff dismissal logic (should delegate)
Refactoring Tasks:
-
Add send_at_mention() Method [CRITICAL]
- Insert after send_selection() or send_message()
- ~25 lines implementation
- Complexity: LOW
- Time: 30 minutes
-
Simplify Diff Handling [MEDIUM]
- Review _handle_file_edited()
- Emit events instead of direct dismissal
- Complexity: LOW
- Time: 1 hour
Expected Result:
- ✅ Complete handler interface implemented
- ✅ Cleaner event-driven architecture
Code Changes: +25 lines, ~15 lines simplified
Current State: Core initialization, mostly clean
Violations:
- ☐ send_selection() may be redundant with protocol
- ☐ Direct protocol internals access in send_message()
- ☐ No protocol interface validation
Refactoring Tasks:
-
Add Protocol Validation [MEDIUM]
- After protocol setup in _init_components()
- Validate each handler has required methods
- Warn if incomplete
- Complexity: LOW
- Time: 1 hour
-
Review send_selection() API [LOW]
- Determine if needed at top level
- May just delegate to protocol
- Complexity: TRIVIAL
- Time: 30 minutes
Expected Result:
- ✅ Early validation catches incomplete handlers
- ✅ Clear API delegation
Code Addition: ~30 lines (validation)
Current State: UI layer with selection extraction
Violations:
- ☐ get_current_selection() should use extracted utility
- ☐ Selection tracking calls handlers directly
- ☐ Could be provider-coupled
Refactoring Tasks:
-
Use Selection Utility [HIGH]
- Replace internal selection logic
- Use:
require('diffusion.utils.selection').get_current_selection() - Complexity: LOW
- Time: 1 hour
-
Emit Events for Selection Changes [MEDIUM]
- Change: Don't call protocol directly
- Emit: "ui:selection_changed" event
- Handlers subscribe if they want tracking
- Complexity: MEDIUM
- Time: 2 hours
Expected Result:
- ✅ Reuses common selection utility
- ✅ Decoupled from protocols
Code Reduction: ~80 lines (reuse utility)
Current State: Configuration with embedded logic
Violations:
- ☐ Auto-mode logic in config (lines 296-336)
- ☐ Modifies handler enabled flags
- ☐ Should be pure data
Refactoring Tasks:
-
Move Auto-Mode Logic [MEDIUM]
- Remove: Lines 296-336 from config
- Move to: Protocol layer's detect_service()
- Config just stores "auto" value
- Complexity: MEDIUM
- Time: 2 hours
-
Add Schema Validation [LOW]
- Type checking, enum validation
- Range validation
- Complexity: MEDIUM
- Time: 4 hours
Expected Result:
- ✅ Config is pure data
- ✅ Better error messages
- ✅ Separation of concerns
Code Reduction: ~40 lines (remove logic)
Current State: HTTP client with business logic
Violations:
- ☐ _handle_file_edited() has dismissal logic
- ☐ _find_diff_for_file() duplicates DiffManager
- ☐ _pending_diffs tracking (should be DiffManager)
- ☐ format_file_reference() is presentation logic
Refactoring Tasks:
-
Simplify Event Handling [MEDIUM]
- _handle_file_edited(): Just emit event
- Remove: Direct dismiss_diff() calls
- Remove: _find_diff_for_file() method
- Complexity: LOW
- Time: 2 hours
-
Remove Diff Tracking [HIGH]
- Delete: _pending_diffs table
- DiffManager should track relationships
- Complexity: MEDIUM
- Time: 3 hours
-
Extract Edit Parser [MEDIUM]
- Create:
utils/diff_parser.lua - Move: Edit details parsing logic
- Complexity: MEDIUM
- Time: 3 hours
- Create:
-
Move Format Function [LOW]
- Move format_file_reference to handler
- Or: utils/opencode_format.lua
- Complexity: TRIVIAL
- Time: 30 minutes
Expected Result:
- ✅ Client is focused on transport
- ✅ Business logic in appropriate layers
- ✅ Easier to test and maintain
Code Reduction: 475 lines (44%)
| File | Current | Target | Reduction | Method |
|---|---|---|---|---|
| protocol/claude.lua | 1,147 | 350 | -797 | Extract to services |
| client/opencode_client.lua | 1,075 | 600 | -475 | Remove business logic |
| plugin/functions.lua | 757 | ~727 | -30 | Use utilities |
| ui/init.lua | ~800 | ~720 | -80 | Use utilities |
| config.lua | 719 | ~679 | -40 | Remove logic |
| TOTAL | 4,498 | 3,076 | -1,422 | 31% reduction |
Between Claude and OpenCode Handlers:
- Lock file reading logic (Claude-specific, but pattern useful)
- Status reporting structure (should be standardized)
- Connection lifecycle (start/stop patterns similar)
Between Commands:
- Multiple commands do initialization checks (DRY opportunity)
- Error notification patterns repeated
- Could extract:
ensure_initialized()helper
Between Client and Handler:
- Error handling patterns
- Event emission patterns
- Logging patterns (could standardize)
- send_message(text) - ✅ Already abstracted
- send_selection(selection_data) - ✅ Already abstracted
- send_at_mention(file, start, end) - ❌ MISSING (Issue #1)
- start() - ✅ Already abstracted
- stop() - ✅ Already abstracted
- is_available() - ✅ Already abstracted
- is_connected() - ✅ Already abstracted
- get_status() - ✅ Already abstracted
Priority 1 - Create New:
-
lua/diffusion/utils/selection.lua- Visual selection extraction -
lua/diffusion/utils/diff_parser.lua- Diff structure parsing -
lua/diffusion/services/tool_service.lua- Tool implementations
Priority 2 - Enhance Existing:
-
lua/diffusion/diff/manager.lua- Add call_id tracking -
lua/diffusion/services/tmux.lua- Add pane discovery methods -
lua/diffusion/ui/init.lua- Event-based selection tracking
Standard Interface (all handlers must implement):
-- Lifecycle
function Handler:start() -> success, error
function Handler:stop() -> success, error
-- Availability
function Handler:is_available() -> boolean
function Handler:is_connected() -> boolean
-- Communication
function Handler:send_message(text) -> success, error
function Handler:send_selection(selection_data) -> success, error
function Handler:send_at_mention(file, start, end) -> success, error
-- Status
function Handler:get_status() -> tableOptional Methods (handler-specific):
- get_tmux_pane() - If tmux integration available
- is_same_tmux_window() - If workspace checking available
- Custom configuration handling
Goal: Establish provider-agnostic foundation
Tasks:
- Create visual selection utility (
utils/selection.lua) - Add
Protocol:send_at_mention()method - Implement
ClaudeHandler:send_at_mention() - Implement
OpenCodeHandler:send_at_mention() - Update
DiffusionSendcommand to use protocol - Add handler interface validation in init
Deliverables:
- ✅ All commands use protocol abstraction
- ✅ No direct handler access from commands
- ✅ Basic interface validation working
Success Criteria:
- DiffusionSend works with both Claude and OpenCode
- Tests pass
- No architectural violations in command layer
Goal: Ensure consistent handler interfaces
Tasks:
- Document standard handler interface
- Add interface compliance tests
- Standardize error return formats
- Standardize status report structure
- Add protocol-level error handling
- Create handler development guide
Deliverables:
- ✅ Documented handler interface contract
- ✅ Validation tests for all handlers
- ✅ Consistent error handling across handlers
Success Criteria:
- All handlers pass interface compliance tests
- Error messages are consistent and helpful
- New handler implementation is straightforward
Goal: Extract business logic from handlers to core services
Major Refactorings:
- Extract Claude tool implementations to ToolService
- Remove diff logic from Claude handler (use DiffManager)
- Extract tmux operations to TmuxService
- Move selection tracking to UI layer
- Simplify OpenCode client event handling
- Remove diff tracking from OpenCode client
- Extract diff parsing to utils
Deliverables:
- ✅
services/tool_service.luacreated - ✅ Claude handler reduced to ~350 lines
- ✅ OpenCode client reduced to ~600 lines
- ✅
utils/diff_parser.luacreated
Success Criteria:
- Handler files meet target sizes
- All business logic in appropriate layers
- Tests pass
- No duplication between handlers
Goal: Final cleanup, optimization, documentation
Tasks:
- Remove auto-mode logic from config
- Add schema validation to config
- Standardize logging across handlers
- Extract duplicate code patterns
- Add integration tests
- Update all documentation
- Performance regression tests
- Update CLAUDE.md with new architecture
Deliverables:
- ✅ Config is pure data
- ✅ Comprehensive test coverage
- ✅ Updated documentation
- ✅ Performance benchmarks maintained
Success Criteria:
- All checkboxes in file headers completed
- Zero architectural violations
- Code reduction target met (1,400+ lines removed)
- Performance maintained or improved
New Utilities:
-
tests/unit/utils/selection_test.lua- Visual marks extraction
- Mode detection
- Fallback behavior
- Edge cases (empty files, unnamed buffers)
-
tests/unit/utils/diff_parser_test.lua- OpenCode edit details parsing
- Claude diff format (if applicable)
- Malformed input handling
Modified Components:
-
tests/unit/protocol/init_test.lua- send_at_mention() routing
- Handler interface validation
- Error handling
-
tests/unit/protocol/claude_test.lua- send_at_mention() implementation
- MCP message format
- Line number conversion (0-based)
-
tests/unit/protocol/opencode_test.lua- send_at_mention() implementation
- @file:line-line format
- HTTP request handling
Provider Switching:
- Test switching between Claude and OpenCode
- Test send_at_mention() with each provider
- Test auto-mode detection
End-to-End:
- Visual selection → DiffusionSend → Claude receives at_mentioned
- Visual selection → DiffusionSend → OpenCode receives @file:line
- Error handling when no provider available
Critical Paths:
- Diff creation and display (existing flow must still work)
- Navigation after diff dismissal
- Selection tracking (continuous monitoring)
- Connection establishment and discovery
1. Claude Handler Refactoring (Phase 3)
- Risk: Breaking existing diff workflow
- Mitigation: Incremental changes, extensive testing after each step
- Rollback: Keep git commits small, test after each extraction
2. Selection Utility Extraction (Phase 1)
- Risk: Subtle behavior differences between implementations
- Mitigation: Comprehensive unit tests, test with real workflows
- Rollback: Easy to revert, single file change
3. Event System Changes (Phase 3)
- Risk: Event listeners not properly subscribed/unsubscribed
- Mitigation: Add debug logging, test cleanup on stop()
- Rollback: Keep event emission patterns simple
4. Config Auto-Mode Logic Move (Phase 4)
- Risk: Service detection behavior changes
- Mitigation: Test auto-mode extensively before/after
- Rollback: Logic is isolated, easy to revert
5. OpenCode Client Simplification (Phase 3)
- Risk: Diff dismissal timing changes
- Mitigation: Test edit workflow end-to-end
- Rollback: Isolated to OpenCode, doesn't affect Claude
6. Protocol Method Addition (Phase 1)
- Risk: Minimal, new method doesn't break existing
- Mitigation: Standard delegation pattern
- Rollback: N/A (new feature)
7. Documentation Updates (Phase 4)
- Risk: None
- Mitigation: N/A
- Rollback: N/A
- Claude handler: ≤ 400 lines (currently 1,147)
- OpenCode client: ≤ 650 lines (currently 1,075)
- Total reduction: ≥ 1,200 lines
- Zero direct handler access from commands
- Zero architectural violations (per file headers)
- DiffusionSend works with Claude
- DiffusionSend works with OpenCode
- Provider switching maintains state correctly
- Auto-mode detects available services
- All existing features still work
- All unit tests pass
- All integration tests pass
- No performance regression
- Code coverage ≥ current level
- Documentation complete and accurate
- All checkboxes in file headers marked complete
- Handler interface standardized and validated
- Business logic in core services only
- Protocols are thin translation layers
- Commands delegate to protocol layer only
Immediate Priorities:
- Add third provider (Gemini or Codex) to validate architecture
- Implement performance optimizations from DIFFUSION_PERFORMANCE_OPTIMIZATION.md
- Add telemetry and monitoring
- Improve error messages and debugging
Long-term Improvements:
- Plugin manager integration testing
- Multi-workspace support
- Offline mode / fallback behavior
- Configuration UI/TUI
- Advanced diff features (3-way merge, conflict resolution)
When adding new features after this refactoring:
DO:
- Add new operations to protocol layer first
- Implement in all handlers consistently
- Write tests before implementation
- Update interface validation
- Follow established patterns
DON'T:
- Access handlers directly from commands
- Put business logic in handlers
- Skip interface validation
- Duplicate code between handlers
- Break provider agnosticism
Track completion of refactoring tasks from file headers:
plugin/functions.lua:
- Extract get_visual_marks() to utils/selection.lua
- Add Protocol:send_at_mention() to protocol/init.lua
- Implement ClaudeHandler:send_at_mention()
- Implement OpenCodeHandler:send_at_mention()
- Update DiffusionSend to use protocol abstraction
- Remove all direct handler access from this file
- Add unit tests for selection utility
lua/diffusion/protocol/init.lua:
- Add Protocol:send_at_mention() method
- Document selection vs at-mention distinction
- Verify all handlers implement standard interface
- Add handler interface validation on initialization
- Update error messages to be provider-agnostic
- Extract handler availability checks to helper method
- Add integration tests for handler switching
lua/diffusion/protocol/claude.lua:
- Implement ClaudeHandler:send_at_mention()
- Extract tool implementations to ToolService
- Move diff logic to DiffManager core
- Move tmux logic to TmuxService
- Move selection tracking to UI layer
- Reduce handler to pure translation (~350 lines)
- Add handler interface compliance tests
lua/diffusion/protocol/opencode.lua:
- Implement OpenCodeHandler:send_at_mention()
- Extract diff dismissal to Navigation module
- Move patch parsing to core diff utilities
- Verify handler implements full protocol interface
- Add error handling for HTTP failures
- Add reconnection logic for SSE stream drops
- Test with multiple OpenCode workspaces
lua/diffusion/init.lua:
- Add protocol interface validation
- Clarify send_selection() vs protocol:send_selection()
- Improve error categorization and messages
- Document component initialization dependencies
- Add integration tests for initialization sequence
- Verify cleanup runs on all exit paths
lua/diffusion/ui/init.lua:
- Extract selection logic to utils/selection.lua
- Create get_visual_marks() utility function
- Update UI layer to use new utility
- Update DiffusionSend to use new utility
- Change selection tracking to emit events
- Add unit tests for selection utility
- Document mode detection behavior
lua/diffusion/config.lua:
- Move auto-mode logic from config to protocol layer
- Document all provider-specific config options
- Add schema validation with helpful error messages
- Extract config merging logic to separate function
- Add config validation tests
- Document configuration layer priority
lua/diffusion/client/opencode_client.lua:
- Simplify _handle_file_edited() to just emit event
- Remove _pending_diffs tracking
- Remove _find_diff_for_file() method
- Move format_file_reference() to handler or utils
- Add reconnection logic for dropped connections
- Add rate limiting for HTTP requests
- Improve error messages with context
Check line counts:
wc -l lua/diffusion/protocol/claude.luaFind TODO comments:
grep -r "TODO\|FIXME\|XXX" lua/Run specific test:
lua tests/unit/utils/selection_test.luaCheck for direct handler access:
grep -r "_handlers\\.claude\|_handlers\\.opencode" lua/ plugin/Validate no provider checks in core:
grep -r 'if.*"claude"\|if.*"opencode"' lua/diffusion/diff/ lua/diffusion/ui/v1.0 - 2025-10-22
- Initial document creation
- Complete analysis of current HEAD
- Detailed refactoring roadmap
- File-by-file task breakdown
- Implementation phases defined
END OF DOCUMENT
This roadmap will be updated as refactoring progresses. Mark checkboxes with ☑ when completed.