-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
721 lines (605 loc) · 22.8 KB
/
Copy pathinit.lua
File metadata and controls
721 lines (605 loc) · 22.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
--[[
================================================================================
PROTOCOL ABSTRACTION LAYER - PROVIDER-AGNOSTIC REFACTORING GUIDE
================================================================================
PURPOSE:
This is the central routing layer that delegates operations to provider-specific
handlers (Claude, OpenCode, Gemini, Codex). It provides a unified API that
the rest of the codebase uses, hiding provider-specific implementation details.
ARCHITECTURAL PRINCIPLES:
1. Single interface for all provider operations
2. Routes method calls to the active handler
3. Handles service switching and availability detection
4. Never contains provider-specific logic
5. All handler methods must have matching signatures across providers
CURRENT ARCHITECTURAL VIOLATIONS:
☑ Missing: send_at_mention() method (needed by DiffusionSend command) - FIXED
☐ send_selection() exists but duplicates at-mention functionality
☐ No clear distinction between selection_changed vs at_mentioned operations
REQUIRED REFACTORING:
1. ADD AT-MENTION ABSTRACTION (Priority: CRITICAL)
ADD TO: lua/diffusion/protocol/init.lua
- Protocol:send_at_mention(file_path, start_line, end_line)
- Get active handler via self:_get_active_handler()
- Check handler.send_at_mention exists
- Delegate to handler:send_at_mention(file_path, start_line, end_line)
- Return success, error_message
- Handle cases where no handler is active
2. CLARIFY SELECTION VS AT-MENTION (Priority: HIGH)
DISTINCTION:
- send_selection(): Continuous tracking, sends selection_changed events
Used by: Real-time selection tracking in UI layer
Providers: Claude (WebSocket notification), OpenCode (not implemented)
- send_at_mention(): User-initiated action, notifies AI of code reference
Used by: :DiffusionSend command, explicit user action
Providers: Claude (MCP at_mentioned), OpenCode (HTTP @file:line prompt)
3. STANDARDIZE HANDLER INTERFACE (Priority: HIGH)
ALL HANDLERS MUST IMPLEMENT:
- start() -> success, error
- stop() -> success, error
- is_available() -> boolean
- is_connected() -> boolean
- send_message(text) -> success, error
- send_selection(selection_data) -> success, error
- send_at_mention(file_path, start_line, end_line) -> success, error
- get_status() -> table
PROVIDER-SPECIFIC VS GENERIC OPERATIONS:
GENERIC (belongs in this file):
- Method routing based on active handler
- Service switching logic
- Availability detection (auto mode)
- Fallback behavior when handlers unavailable
- Error aggregation and logging
- Handler lifecycle management (start/stop all)
PROVIDER-SPECIFIC (belongs in handler files):
- Connection establishment (WebSocket vs HTTP)
- Message format serialization (MCP vs HTTP JSON)
- Authentication mechanisms
- Transport-specific error handling
- Provider-specific status fields
HANDLER METHOD SIGNATURES THAT MUST MATCH:
Claude (protocol/claude.lua):
:send_at_mention(file_path, start_line, end_line)
- Converts to 0-based line numbers
- Sends MCP at_mentioned message via WebSocket
- Focuses tmux pane if configured
OpenCode (protocol/opencode.lua):
:send_at_mention(file_path, start_line, end_line)
- Formats as @file:start_line-end_line
- Sends via HTTP prompt endpoint
- No tmux integration (OpenCode handles UI)
REFACTORING CHECKLIST:
☑ 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
☐ Update this header as refactoring progresses
NOTE: This header must be updated as each refactoring task is completed.
Mark checkboxes with ☑ when done.
================================================================================
--]]
-- Protocol abstraction layer for diffusion.nvim
-- Provides unified interface for Claude Code MCP and OpenCode protocols
local Protocol = {}
function Protocol:new(config, events, diff_manager)
local ProviderLock = require('diffusion.protocol.provider_lock')
local allow_multiple = config.services and config.services.allow_multiple_providers == true
local instance = {
_config = config.services,
_events = events,
_diff_manager = diff_manager,
_logger = require('diffusion.utils.logger'):child("Protocol"),
_handlers = {},
_current_handler = nil,
_handler_validation = {}, -- Store validation results for debugging
_provider_lock = ProviderLock:new(allow_multiple),
_stats = {
messages_sent = 0,
service_switches = 0,
connection_failures = 0
},
_event_unsubs = {},
}
setmetatable(instance, { __index = self })
return instance
end
function Protocol:setup()
self._logger:info("Setting up protocol layer")
-- Initialize service handlers
self:_init_handlers()
-- Setup event listeners
self:_setup_events()
-- Don't start service during setup - wait for explicit start() call
-- This allows auto_start to control when services actually start
self._logger:info("Protocol layer setup complete")
end
function Protocol:_init_handlers()
-- Initialize Claude handler
self._logger:debug("Checking Claude config", {
has_claude_config = self._config.claude ~= nil,
claude_enabled = self._config.claude and self._config.claude.enabled or false
})
if self._config.claude and self._config.claude.enabled then
local ClaudeHandler = require('diffusion.protocol.claude')
self._handlers.claude = ClaudeHandler:new(self._config.claude, self._events, self._diff_manager, self._provider_lock)
self._logger:debug("Claude handler initialized")
else
self._logger:info("Claude handler SKIPPED (disabled in config)")
end
-- Initialize Gemini handler
if self._config.gemini and self._config.gemini.enabled then
local GeminiHandler = require('diffusion.protocol.gemini')
self._handlers.gemini = GeminiHandler:new(self._config, self._events, self._diff_manager)
self._logger:debug("Gemini handler initialized")
end
-- Initialize OpenCode handler
self._logger:debug("Checking OpenCode config", {
has_opencode_config = self._config.opencode ~= nil,
opencode_enabled = self._config.opencode and self._config.opencode.enabled or false
})
if self._config.opencode and self._config.opencode.enabled then
local OpenCodeHandler = require('diffusion.protocol.opencode')
self._handlers.opencode = OpenCodeHandler:new(self._config.opencode, self._events, self._diff_manager)
self._logger:info("✅ OpenCode handler initialized")
else
self._logger:debug("OpenCode handler skipped (disabled in config)")
end
-- Initialize Codex handler
if self._config.codex and self._config.codex.enabled then
local CodexHandler = require('diffusion.protocol.codex')
self._handlers.codex = CodexHandler:new(self._config.codex, self._events, self._diff_manager, self._provider_lock)
self._logger:debug("Codex handler initialized")
end
-- Initialize Pi handler
if self._config.pi and self._config.pi.enabled then
local PiHandler = require('diffusion.protocol.pi')
self._handlers.pi = PiHandler:new(self._config.pi, self._events, self._diff_manager, self._provider_lock)
self._logger:info("Pi handler initialized")
else
self._logger:debug("Pi handler skipped (disabled in config)")
end
-- Validate handler interfaces after initialization
self:_validate_handler_interfaces()
end
-- Validate that all handlers implement the required interface
function Protocol:_validate_handler_interfaces()
-- Required methods that all handlers must implement
local required_methods = {
'start',
'stop',
'is_available',
'is_connected',
'send_message',
'send_selection',
'send_at_mention',
'get_status'
}
self._logger:info("Validating handler interfaces...")
for handler_name, handler in pairs(self._handlers) do
local missing_methods = {}
local validation_result = {
complete = true,
missing_methods = {}
}
-- Check each required method
for _, method in ipairs(required_methods) do
if type(handler[method]) ~= 'function' then
table.insert(missing_methods, method)
validation_result.complete = false
table.insert(validation_result.missing_methods, method)
self._logger:warn(string.format(
"Handler '%s' missing required method: %s",
handler_name,
method
))
end
end
-- Store validation result
self._handler_validation[handler_name] = validation_result
if #missing_methods == 0 then
self._logger:info(string.format(
"✅ Handler '%s' implements complete interface",
handler_name
))
else
self._logger:warn(string.format(
"⚠️ Handler '%s' missing %d methods: %s",
handler_name,
#missing_methods,
table.concat(missing_methods, ", ")
))
end
end
-- Log summary
local complete_handlers = 0
local incomplete_handlers = 0
for _, validation in pairs(self._handler_validation) do
if validation.complete then
complete_handlers = complete_handlers + 1
else
incomplete_handlers = incomplete_handlers + 1
end
end
self._logger:info(string.format(
"Handler validation complete: %d complete, %d incomplete",
complete_handlers,
incomplete_handlers
))
end
function Protocol:_setup_events()
-- Clean up any prior listeners from a previous start/stop cycle
for _, unsub in ipairs(self._event_unsubs) do
unsub()
end
self._event_unsubs = {}
-- Listen for handler events (store unsub functions for cleanup)
self._event_unsubs[#self._event_unsubs + 1] = self._events:on("handler:connection_lost", function(data)
self._stats.connection_failures = self._stats.connection_failures + 1
self:_handle_connection_lost(data.service)
end)
self._event_unsubs[#self._event_unsubs + 1] = self._events:on("handler:authenticated", function(data)
self._logger:info("Handler authenticated", { service = data.service })
end)
end
-- Start the protocol layer
function Protocol:start()
self._logger:info("🚀 Starting protocol layer")
-- Always detect and switch to the configured service
local service = self:detect_service()
self._logger:info("Detected service", { service = service })
if service then
self._logger:info("Switching to detected service", { service = service })
local success, err = self:switch_service(service)
if not success then
self._logger:error("Failed to switch service", { service = service, error = err })
return false, err
end
self._logger:info("✅ Successfully switched to service", { service = service })
else
self._logger:error("No available services detected")
return false, "No available services"
end
-- Start every WS-based handler as a passive listener so its lockfile is
-- written and clients can discover it. The provider_lock enforces
-- "first connection wins" between them when allow_multiple_providers is
-- false. OpenCode uses HTTP and stays under the switch_service mutex.
local ws_handlers = { "claude", "codex", "pi" }
for _, name in ipairs(ws_handlers) do
local handler = self._handlers[name]
if handler and handler ~= self._current_handler then
local ok, err = handler:start()
if ok then
self._logger:info("Started passive WS listener", { service = name })
else
self._logger:warn("Passive WS listener failed to start", { service = name, error = err })
end
end
end
self._logger:info("✅ Protocol layer started successfully", {
service = self:get_active_service(),
handler_running = self._current_handler and self._current_handler:is_connected()
})
return true
end
-- Stop the protocol layer
function Protocol:stop()
self._logger:info("Stopping protocol layer")
if self._current_handler then
self._current_handler:stop()
end
-- Stop all handlers
for service, handler in pairs(self._handlers) do
if handler.stop then
handler:stop()
end
end
-- Unsubscribe event listeners to prevent accumulation
for _, unsub in ipairs(self._event_unsubs) do
unsub()
end
self._event_unsubs = {}
self._logger:info("Protocol layer stopped")
end
-- Detect available service
function Protocol:detect_service()
-- Prefer explicit default if set (non-auto mode)
if self._config.default and self._config.default ~= "auto" then
local preferred = self._handlers[self._config.default]
if preferred then
self._logger:info("Using configured default service", { service = self._config.default })
return self._config.default
else
self._logger:warn("Configured default service not available", {
default = self._config.default,
available_handlers = vim.tbl_keys(self._handlers)
})
end
end
-- Auto-detection mode: check actual availability
if self._config.default == "auto" then
self._logger:info("🔍 Auto-detecting available service...")
-- Check in priority order: Claude -> Gemini -> OpenCode
local order = { "claude", "gemini", "opencode" }
for _, name in ipairs(order) do
local handler = self._handlers[name]
if handler then
self._logger:info("Checking availability for: " .. name)
-- Check if service is actually available (has active connection)
if handler.is_available and handler:is_available() then
self._logger:info("✅ Auto-detected available service", { service = name })
return name
else
self._logger:info("❌ Service not available: " .. name)
end
else
self._logger:debug("Handler not initialized: " .. name)
end
end
-- Fallback: if no service is actively available, pick first enabled handler
self._logger:debug("No actively available services detected, using first enabled handler")
for _, name in ipairs(order) do
if self._handlers[name] then
self._logger:info("Falling back to first enabled handler", { service = name })
return name
end
end
return nil
end
-- Default behavior: pick the first enabled handler in a stable order
local order = { "claude", "gemini", "opencode" }
for _, name in ipairs(order) do
if self._handlers[name] then
self._logger:info("Auto-selecting service", { service = name })
return name
end
end
return nil
end
-- Switch to specific service
function Protocol:switch_service(service)
if service == "auto" then
service = self:detect_service()
if not service then
self._logger:error("No services available for auto-detection")
return false
end
end
local handler = self._handlers[service]
if not handler then
self._logger:error("Unknown service", { service = service })
return false
end
-- Even if not currently available, allow switching so the handler can start
if not handler:is_available() then
self._logger:debug("Service not yet available; will attempt to start", { service = service })
end
local previous_service = self._current_handler and self:get_active_service() or nil
-- Stop current handler
if self._current_handler and self._current_handler.stop then
self._logger:info("Stopping previous service", { service = previous_service })
self._current_handler:stop()
end
-- Switch to new handler
self._current_handler = handler
if previous_service ~= service then
self._stats.service_switches = self._stats.service_switches + 1
self._logger:info("Switched service", {
from = previous_service,
to = service
})
self._events:emit("service_switched", {
from = previous_service,
to = service,
reason = "manual"
})
end
-- Start the new handler
if handler.start then
self._logger:info("Starting new service handler", { service = service })
local success, err = handler:start()
if not success then
self._logger:error("Failed to start handler", { service = service, error = err })
return false, err
end
self._logger:info("Handler started successfully", { service = service })
end
return true
end
-- Send selection to active service
function Protocol:send_selection(selection_data)
if not self._current_handler then
self._logger:error("No active service handler")
return false
end
self._stats.messages_sent = self._stats.messages_sent + 1
return self._current_handler:send_selection(selection_data)
end
-- Send message to active service
function Protocol:send_message(message)
if not self._current_handler then
self._logger:error("No active service handler")
return false
end
self._stats.messages_sent = self._stats.messages_sent + 1
return self._current_handler:send_message(message)
end
-- Send at-mention notification to active service
-- This is a user-initiated action that notifies the AI about a specific code reference
-- @param file_path string: Absolute path to the file
-- @param start_line number: Starting line number (1-based)
-- @param end_line number: Ending line number (1-based)
-- @return boolean: Success status
-- @return string|nil: Error message if failed
function Protocol:send_at_mention(file_path, start_line, end_line)
if not self._current_handler then
self._logger:error("No active service handler")
return false, "No active service handler available"
end
-- Check if handler implements send_at_mention
if not self._current_handler.send_at_mention then
local service = self:get_active_service()
self._logger:error("Handler does not support send_at_mention", { service = service })
return false, "Service '" .. (service or "unknown") .. "' does not support at-mention notifications"
end
-- Validate parameters
if not file_path or file_path == "" then
self._logger:error("Invalid file_path for send_at_mention", { file_path = file_path })
return false, "Invalid file path"
end
if not start_line or start_line < 1 then
self._logger:error("Invalid start_line for send_at_mention", { start_line = start_line })
return false, "Invalid start line"
end
if not end_line or end_line < start_line then
self._logger:error("Invalid end_line for send_at_mention", {
start_line = start_line,
end_line = end_line
})
return false, "Invalid end line"
end
self._logger:debug("Sending at-mention to active handler", {
service = self:get_active_service(),
file_path = file_path,
start_line = start_line,
end_line = end_line
})
self._stats.messages_sent = self._stats.messages_sent + 1
return self._current_handler:send_at_mention(file_path, start_line, end_line)
end
-- Open diff via active service
function Protocol:open_diff(diff_params)
if not self._current_handler then
self._logger:error("No active service handler")
return false
end
return self._current_handler:open_diff(diff_params)
end
-- Get active service name
function Protocol:get_active_service()
if not self._current_handler then
return nil
end
for service, handler in pairs(self._handlers) do
if handler == self._current_handler then
return service
end
end
return nil
end
-- Get service status
function Protocol:get_service_status(service)
local handler = self._handlers[service]
if not handler then
return { available = false, error = "Unknown service" }
end
local has_is_connected = type(handler.is_connected) == 'function'
local status = {
available = handler:is_available(),
active = handler == self._current_handler,
connected = has_is_connected and handler:is_connected() or false
}
-- Include detailed status if handler provides it
if type(handler.get_status) == 'function' then
local detailed = handler:get_status()
if detailed then
-- Merge detailed status (including nested structures like http/mcp for OpenCode)
status = vim.tbl_deep_extend('force', status, detailed)
end
end
return status
end
-- Get all service statuses
function Protocol:get_all_service_statuses()
local statuses = {}
for service, handler in pairs(self._handlers) do
statuses[service] = self:get_service_status(service)
end
return statuses
end
-- Handle connection lost
function Protocol:_handle_connection_lost(service)
self._logger:warn("Connection lost", { service = service })
-- If this is the current service, try to switch to another
if self:get_active_service() == service and self._config.fallback then
self._logger:info("Attempting service fallback")
local alternative_service = self:detect_service()
if alternative_service and alternative_service ~= service then
local success = self:switch_service(alternative_service)
if success then
self._logger:info("Switched to fallback service", {
service = alternative_service
})
self._events:emit("service_switched", {
from = service,
to = alternative_service,
reason = "connection_lost"
})
end
end
end
end
-- Get protocol statistics
function Protocol:get_stats()
local stats = vim.deepcopy(self._stats)
stats.active_service = self:get_active_service()
stats.available_services = {}
for service, handler in pairs(self._handlers) do
stats.available_services[service] = handler:is_available()
end
return stats
end
-- Check if any service is available
function Protocol:has_available_service()
for _, handler in pairs(self._handlers) do
if handler:is_available() then
return true
end
end
return false
end
-- Get supported tools for active service
function Protocol:get_available_tools()
if not self._current_handler then
return {}
end
if self._current_handler.get_available_tools then
return self._current_handler:get_available_tools()
end
return {}
end
-- Execute tool via active service
function Protocol:execute_tool(tool_name, args)
if not self._current_handler then
self._logger:error("No active service handler")
return false, "No active service"
end
if not self._current_handler.execute_tool then
return false, "Tool execution not supported by current service"
end
return self._current_handler:execute_tool(tool_name, args)
end
-- Validate configuration
function Protocol:validate_config()
local errors = {}
-- Check that at least one service is enabled
local enabled_services = 0
for service, config in pairs(self._config) do
if type(config) == "table" and config.enabled then
enabled_services = enabled_services + 1
end
end
if enabled_services == 0 then
table.insert(errors, "At least one service must be enabled")
end
-- Validate default service
if self._config.default and
self._config.default ~= "auto" and
not self._config[self._config.default] then
table.insert(errors, "Default service '" .. self._config.default .. "' is not configured")
end
return #errors == 0, errors
end
return Protocol