codetether 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. ui/monitor.js +2662 -0
ui/monitor.js ADDED
@@ -0,0 +1,2662 @@
1
+ // A2A Agent Monitor - Real-time monitoring and intervention
2
+ class AgentMonitor {
3
+ constructor() {
4
+ this.messages = [];
5
+ this.agents = new Map();
6
+ this.tasks = [];
7
+ this.codebases = []; // OpenCode registered codebases
8
+ this.models = []; // Available AI models
9
+ this.defaultModel = '';
10
+ this.currentTaskFilter = 'all';
11
+ this.eventSource = null;
12
+ this.isPaused = false;
13
+ this.currentFilter = 'all';
14
+ this.totalStoredMessages = 0;
15
+ this.stats = {
16
+ totalMessages: 0,
17
+ interventions: 0,
18
+ toolCalls: 0,
19
+ errors: 0,
20
+ tokens: 0,
21
+ responseTimes: []
22
+ };
23
+
24
+ // Agent output tracking
25
+ this.agentOutputStreams = new Map(); // codebase_id -> EventSource
26
+ this.agentOutputs = new Map(); // codebase_id -> output entries array
27
+ this.currentOutputAgent = null; // Currently selected agent for output view
28
+ this.autoScroll = true; // Auto-scroll output
29
+ this.streamingParts = new Map(); // part_id -> element for streaming updates
30
+
31
+ this.init();
32
+ }
33
+
34
+ init() {
35
+ this.connectToServer();
36
+ this.setupEventListeners();
37
+ this.startStatsUpdate();
38
+ this.pollTaskQueue();
39
+ this.fetchTotalMessageCount();
40
+ this.loadModels(); // Load available models
41
+ this.initOpenCode(); // Initialize OpenCode integration
42
+ this.initAgentOutput(); // Initialize agent output panel
43
+ }
44
+
45
+ async loadModels() {
46
+ try {
47
+ const serverUrl = this.getServerUrl();
48
+ const response = await fetch(`${serverUrl}/v1/opencode/models`);
49
+ if (response.ok) {
50
+ const data = await response.json();
51
+ this.models = data.models || [];
52
+ this.defaultModel = data.default || '';
53
+ // Re-render codebases to update selectors if they are already shown
54
+ if (this.codebases.length > 0) {
55
+ this.displayCodebases();
56
+ }
57
+ }
58
+ } catch (error) {
59
+ console.error('Failed to load models:', error);
60
+ }
61
+ }
62
+
63
+ renderModelOptions() {
64
+ if (!this.models || this.models.length === 0) {
65
+ return '';
66
+ }
67
+
68
+ // Group models by provider
69
+ const byProvider = {};
70
+ this.models.forEach(m => {
71
+ const provider = m.provider || 'Other';
72
+ if (!byProvider[provider]) byProvider[provider] = [];
73
+ byProvider[provider].push(m);
74
+ });
75
+
76
+ let html = '';
77
+
78
+ // Custom models first
79
+ const customModels = this.models.filter(m => m.custom);
80
+ if (customModels.length > 0) {
81
+ html += '<optgroup label="⭐ Custom (from config)">';
82
+ customModels.forEach(m => {
83
+ const selected = m.id === this.defaultModel ? 'selected' : '';
84
+ const badge = m.capabilities?.reasoning ? ' 🧠' : '';
85
+ html += `<option value="${m.id}" ${selected}>${m.name}${badge}</option>`;
86
+ });
87
+ html += '</optgroup>';
88
+ }
89
+
90
+ // Standard providers
91
+ const standardProviders = ['Anthropic', 'OpenAI', 'Google', 'DeepSeek', 'xAI', 'Z.AI Coding Plan', 'Azure AI Foundry'];
92
+ standardProviders.forEach(provider => {
93
+ const models = (byProvider[provider] || []).filter(m => !m.custom);
94
+ if (models.length > 0) {
95
+ html += `<optgroup label="${provider}">`;
96
+ models.forEach(m => {
97
+ const selected = m.id === this.defaultModel ? 'selected' : '';
98
+ html += `<option value="${m.id}" ${selected}>${m.name}</option>`;
99
+ });
100
+ html += '</optgroup>';
101
+ }
102
+ });
103
+
104
+ // Other providers
105
+ Object.keys(byProvider).forEach(provider => {
106
+ if (!standardProviders.includes(provider) && provider !== 'Other') {
107
+ const models = byProvider[provider].filter(m => !m.custom);
108
+ if (models.length > 0) {
109
+ html += `<optgroup label="${provider}">`;
110
+ models.forEach(m => {
111
+ const selected = m.id === this.defaultModel ? 'selected' : '';
112
+ html += `<option value="${m.id}" ${selected}>${m.name}</option>`;
113
+ });
114
+ html += '</optgroup>';
115
+ }
116
+ }
117
+ });
118
+
119
+ return html;
120
+ }
121
+
122
+ // ========================================
123
+ // Agent Output Panel
124
+ // ========================================
125
+
126
+ initAgentOutput() {
127
+ // Update agent selector when codebases change
128
+ this.updateAgentOutputSelector();
129
+ }
130
+
131
+ updateAgentOutputSelector() {
132
+ const select = document.getElementById('outputAgentSelect');
133
+ if (!select) return;
134
+
135
+ const currentValue = select.value;
136
+ select.innerHTML = '<option value="">Select an agent to view output...</option>';
137
+
138
+ this.codebases.forEach(cb => {
139
+ const option = document.createElement('option');
140
+ option.value = cb.id;
141
+ option.textContent = `${cb.name} (${cb.status})`;
142
+ if (cb.status === 'busy' || cb.status === 'running') {
143
+ option.textContent += ' 🟢';
144
+ }
145
+ select.appendChild(option);
146
+ });
147
+
148
+ // Restore selection if still valid
149
+ if (currentValue && this.codebases.find(cb => cb.id === currentValue)) {
150
+ select.value = currentValue;
151
+ }
152
+ }
153
+
154
+ switchAgentOutput() {
155
+ const select = document.getElementById('outputAgentSelect');
156
+ const codebaseId = select.value;
157
+
158
+ if (!codebaseId) {
159
+ this.currentOutputAgent = null;
160
+ this.displayAgentOutput(null);
161
+ return;
162
+ }
163
+
164
+ this.currentOutputAgent = codebaseId;
165
+
166
+ // Initialize output array if needed
167
+ if (!this.agentOutputs.has(codebaseId)) {
168
+ this.agentOutputs.set(codebaseId, []);
169
+ }
170
+
171
+ // Connect to event stream if not already connected
172
+ if (!this.agentOutputStreams.has(codebaseId)) {
173
+ this.connectAgentEventStream(codebaseId);
174
+ }
175
+
176
+ // Display existing output
177
+ this.displayAgentOutput(codebaseId);
178
+
179
+ // Load existing messages
180
+ this.loadAgentMessages(codebaseId);
181
+ }
182
+
183
+ async connectAgentEventStream(codebaseId) {
184
+ const serverUrl = this.getServerUrl();
185
+ const eventSource = new EventSource(`${serverUrl}/v1/opencode/codebases/${codebaseId}/events`);
186
+
187
+ this.agentOutputStreams.set(codebaseId, eventSource);
188
+
189
+ eventSource.onopen = () => {
190
+ console.log(`Connected to event stream for ${codebaseId}`);
191
+ this.addOutputEntry(codebaseId, {
192
+ type: 'status',
193
+ content: 'Connected to agent event stream',
194
+ timestamp: new Date().toISOString()
195
+ });
196
+ };
197
+
198
+ eventSource.onerror = (error) => {
199
+ console.error(`Event stream error for ${codebaseId}:`, error);
200
+ if (eventSource.readyState === EventSource.CLOSED) {
201
+ this.agentOutputStreams.delete(codebaseId);
202
+ this.addOutputEntry(codebaseId, {
203
+ type: 'status',
204
+ content: 'Disconnected from agent event stream',
205
+ timestamp: new Date().toISOString()
206
+ });
207
+ }
208
+ };
209
+
210
+ // Handle different event types
211
+ eventSource.addEventListener('connected', (e) => {
212
+ const data = JSON.parse(e.data);
213
+ this.addOutputEntry(codebaseId, {
214
+ type: 'status',
215
+ content: `Connected to agent (${data.codebase_id})`,
216
+ timestamp: new Date().toISOString()
217
+ });
218
+ });
219
+
220
+ eventSource.addEventListener('part.text', (e) => {
221
+ this.handleTextPart(codebaseId, JSON.parse(e.data));
222
+ });
223
+
224
+ eventSource.addEventListener('part.reasoning', (e) => {
225
+ this.handleReasoningPart(codebaseId, JSON.parse(e.data));
226
+ });
227
+
228
+ eventSource.addEventListener('part.tool', (e) => {
229
+ this.handleToolPart(codebaseId, JSON.parse(e.data));
230
+ });
231
+
232
+ eventSource.addEventListener('part.step-start', (e) => {
233
+ const data = JSON.parse(e.data);
234
+ this.addOutputEntry(codebaseId, {
235
+ type: 'step-start',
236
+ content: 'Step started',
237
+ timestamp: new Date().toISOString()
238
+ });
239
+ });
240
+
241
+ eventSource.addEventListener('part.step-finish', (e) => {
242
+ const data = JSON.parse(e.data);
243
+ this.addOutputEntry(codebaseId, {
244
+ type: 'step-finish',
245
+ content: `Step finished: ${data.reason || 'complete'}`,
246
+ tokens: data.tokens,
247
+ cost: data.cost,
248
+ timestamp: new Date().toISOString()
249
+ });
250
+ });
251
+
252
+ eventSource.addEventListener('status', (e) => {
253
+ const data = JSON.parse(e.data);
254
+ this.addOutputEntry(codebaseId, {
255
+ type: 'status',
256
+ content: `Status: ${data.status}${data.agent ? ` (${data.agent})` : ''}${data.message ? ` - ${data.message}` : ''}`,
257
+ timestamp: new Date().toISOString()
258
+ });
259
+ // Update codebase status
260
+ this.loadCodebases();
261
+ });
262
+
263
+ // Handle message events from remote workers (task results)
264
+ eventSource.addEventListener('message', (e) => {
265
+ const data = JSON.parse(e.data);
266
+ // Parse nested JSON if content is a string containing JSON events
267
+ if (data.type === 'text' && data.content) {
268
+ try {
269
+ // Content may be newline-separated JSON events from OpenCode
270
+ const lines = data.content.split('\n').filter(l => l.trim());
271
+ for (const line of lines) {
272
+ const event = JSON.parse(line);
273
+ this.processOpenCodeEvent(codebaseId, event);
274
+ }
275
+ } catch {
276
+ // Not JSON, show as plain text
277
+ this.addOutputEntry(codebaseId, {
278
+ type: 'text',
279
+ content: data.content,
280
+ timestamp: new Date().toISOString()
281
+ });
282
+ }
283
+ } else if (data.type) {
284
+ this.processOpenCodeEvent(codebaseId, data);
285
+ }
286
+ });
287
+
288
+ eventSource.addEventListener('idle', (e) => {
289
+ this.addOutputEntry(codebaseId, {
290
+ type: 'status',
291
+ content: 'Agent is now idle',
292
+ timestamp: new Date().toISOString()
293
+ });
294
+ this.loadCodebases();
295
+ });
296
+
297
+ eventSource.addEventListener('file_edit', (e) => {
298
+ const data = JSON.parse(e.data);
299
+ this.addOutputEntry(codebaseId, {
300
+ type: 'file-edit',
301
+ content: `File edited: ${data.path}`,
302
+ timestamp: new Date().toISOString()
303
+ });
304
+ });
305
+
306
+ eventSource.addEventListener('command', (e) => {
307
+ const data = JSON.parse(e.data);
308
+ this.addOutputEntry(codebaseId, {
309
+ type: 'command',
310
+ content: `Command: ${data.command}`,
311
+ output: data.output,
312
+ exitCode: data.exit_code,
313
+ timestamp: new Date().toISOString()
314
+ });
315
+ });
316
+
317
+ eventSource.addEventListener('diagnostics', (e) => {
318
+ const data = JSON.parse(e.data);
319
+ if (data.diagnostics && data.diagnostics.length > 0) {
320
+ this.addOutputEntry(codebaseId, {
321
+ type: 'diagnostics',
322
+ content: `Diagnostics for ${data.path}: ${data.diagnostics.length} issues`,
323
+ diagnostics: data.diagnostics,
324
+ timestamp: new Date().toISOString()
325
+ });
326
+ }
327
+ });
328
+
329
+ eventSource.addEventListener('message', (e) => {
330
+ const data = JSON.parse(e.data);
331
+ // Message metadata update
332
+ if (data.tokens) {
333
+ this.stats.tokens += (data.tokens.input || 0) + (data.tokens.output || 0);
334
+ this.updateStatsDisplay();
335
+ }
336
+ });
337
+
338
+ eventSource.addEventListener('error', (e) => {
339
+ // Only parse if we have data (SSE error event vs connection error)
340
+ if (e.data) {
341
+ try {
342
+ const data = JSON.parse(e.data);
343
+ this.addOutputEntry(codebaseId, {
344
+ type: 'error',
345
+ content: `Error: ${data.error}`,
346
+ timestamp: new Date().toISOString()
347
+ });
348
+ } catch (parseErr) {
349
+ console.warn('Could not parse error data:', e.data);
350
+ }
351
+ }
352
+ });
353
+ }
354
+
355
+ processOpenCodeEvent(codebaseId, event) {
356
+ // Process OpenCode event format from task results
357
+ const type = event.type;
358
+ const part = event.part || {};
359
+
360
+ switch (type) {
361
+ case 'text':
362
+ if (part.text) {
363
+ this.addOutputEntry(codebaseId, {
364
+ type: 'text',
365
+ content: part.text,
366
+ timestamp: new Date().toISOString()
367
+ });
368
+ }
369
+ break;
370
+ case 'tool_use':
371
+ if (part.tool) {
372
+ const state = part.state || {};
373
+ this.addOutputEntry(codebaseId, {
374
+ type: 'tool',
375
+ tool: part.tool,
376
+ status: state.status || 'completed',
377
+ title: state.title || state.input?.description || part.tool,
378
+ input: state.input,
379
+ output: state.output || state.metadata?.output,
380
+ timestamp: new Date().toISOString()
381
+ });
382
+ }
383
+ break;
384
+ case 'step_start':
385
+ this.addOutputEntry(codebaseId, {
386
+ type: 'step-start',
387
+ content: 'Step started',
388
+ timestamp: new Date().toISOString()
389
+ });
390
+ break;
391
+ case 'step_finish':
392
+ this.addOutputEntry(codebaseId, {
393
+ type: 'step-finish',
394
+ content: `Step finished: ${part.reason || 'complete'}`,
395
+ tokens: part.tokens,
396
+ cost: part.cost,
397
+ timestamp: new Date().toISOString()
398
+ });
399
+ break;
400
+ default:
401
+ // Unknown event type, show raw if it has content
402
+ if (part.text || event.content) {
403
+ this.addOutputEntry(codebaseId, {
404
+ type: 'info',
405
+ content: part.text || event.content,
406
+ timestamp: new Date().toISOString()
407
+ });
408
+ }
409
+ }
410
+ }
411
+
412
+ handleTextPart(codebaseId, data) {
413
+ const partId = data.part_id;
414
+
415
+ if (data.delta) {
416
+ // Streaming update - append to existing entry
417
+ let entry = this.getStreamingEntry(codebaseId, partId);
418
+ if (entry) {
419
+ entry.content += data.delta;
420
+ this.updateStreamingElement(partId, entry.content);
421
+ } else {
422
+ // First chunk
423
+ entry = {
424
+ id: partId,
425
+ type: 'text',
426
+ content: data.delta,
427
+ streaming: true,
428
+ timestamp: new Date().toISOString()
429
+ };
430
+ this.addOutputEntry(codebaseId, entry, true);
431
+ }
432
+ } else if (data.text) {
433
+ // Complete text
434
+ const existingEntry = this.getStreamingEntry(codebaseId, partId);
435
+ if (existingEntry) {
436
+ existingEntry.content = data.text;
437
+ existingEntry.streaming = false;
438
+ this.updateStreamingElement(partId, data.text, false);
439
+ } else {
440
+ this.addOutputEntry(codebaseId, {
441
+ id: partId,
442
+ type: 'text',
443
+ content: data.text,
444
+ timestamp: new Date().toISOString()
445
+ });
446
+ }
447
+ }
448
+ }
449
+
450
+ handleReasoningPart(codebaseId, data) {
451
+ const partId = data.part_id;
452
+
453
+ if (data.delta) {
454
+ let entry = this.getStreamingEntry(codebaseId, partId);
455
+ if (entry) {
456
+ entry.content += data.delta;
457
+ this.updateStreamingElement(partId, entry.content);
458
+ } else {
459
+ entry = {
460
+ id: partId,
461
+ type: 'reasoning',
462
+ content: data.delta,
463
+ streaming: true,
464
+ timestamp: new Date().toISOString()
465
+ };
466
+ this.addOutputEntry(codebaseId, entry, true);
467
+ }
468
+ } else if (data.text) {
469
+ const existingEntry = this.getStreamingEntry(codebaseId, partId);
470
+ if (existingEntry) {
471
+ existingEntry.content = data.text;
472
+ existingEntry.streaming = false;
473
+ this.updateStreamingElement(partId, data.text, false);
474
+ } else {
475
+ this.addOutputEntry(codebaseId, {
476
+ id: partId,
477
+ type: 'reasoning',
478
+ content: data.text,
479
+ timestamp: new Date().toISOString()
480
+ });
481
+ }
482
+ }
483
+ }
484
+
485
+ handleToolPart(codebaseId, data) {
486
+ const partId = data.part_id;
487
+ const status = data.status;
488
+
489
+ // Find or create tool entry
490
+ let outputs = this.agentOutputs.get(codebaseId) || [];
491
+ let entry = outputs.find(e => e.id === partId);
492
+
493
+ if (!entry) {
494
+ entry = {
495
+ id: partId,
496
+ type: `tool-${status}`,
497
+ toolName: data.tool_name,
498
+ callId: data.call_id,
499
+ input: data.input,
500
+ output: null,
501
+ error: null,
502
+ title: data.title,
503
+ status: status,
504
+ timestamp: new Date().toISOString()
505
+ };
506
+ this.addOutputEntry(codebaseId, entry);
507
+ } else {
508
+ // Update existing entry
509
+ entry.type = `tool-${status}`;
510
+ entry.status = status;
511
+ entry.title = data.title || entry.title;
512
+ if (data.output) entry.output = data.output;
513
+ if (data.error) entry.error = data.error;
514
+ if (data.metadata) entry.metadata = data.metadata;
515
+
516
+ // Update display
517
+ this.updateToolElement(partId, entry);
518
+ }
519
+
520
+ // Update stats
521
+ if (status === 'completed' || status === 'error') {
522
+ this.stats.toolCalls++;
523
+ if (status === 'error') this.stats.errors++;
524
+ this.updateStatsDisplay();
525
+ }
526
+ }
527
+
528
+ getStreamingEntry(codebaseId, partId) {
529
+ const outputs = this.agentOutputs.get(codebaseId);
530
+ if (!outputs) return null;
531
+ return outputs.find(e => e.id === partId && e.streaming);
532
+ }
533
+
534
+ addOutputEntry(codebaseId, entry, isStreaming = false) {
535
+ if (!this.agentOutputs.has(codebaseId)) {
536
+ this.agentOutputs.set(codebaseId, []);
537
+ }
538
+
539
+ const outputs = this.agentOutputs.get(codebaseId);
540
+
541
+ // Avoid duplicates
542
+ if (entry.id && outputs.find(e => e.id === entry.id)) {
543
+ return;
544
+ }
545
+
546
+ outputs.push(entry);
547
+
548
+ // Limit stored entries
549
+ if (outputs.length > 500) {
550
+ outputs.shift();
551
+ }
552
+
553
+ // Display if this is the current agent
554
+ if (codebaseId === this.currentOutputAgent) {
555
+ this.renderOutputEntry(entry, isStreaming);
556
+ }
557
+ }
558
+
559
+ displayAgentOutput(codebaseId) {
560
+ const container = document.getElementById('agentOutputContainer');
561
+ const noOutputMsg = document.getElementById('noOutputMessage');
562
+
563
+ if (!codebaseId) {
564
+ container.innerHTML = '';
565
+ container.appendChild(noOutputMsg);
566
+ noOutputMsg.style.display = 'block';
567
+ return;
568
+ }
569
+
570
+ const outputs = this.agentOutputs.get(codebaseId) || [];
571
+
572
+ if (outputs.length === 0) {
573
+ container.innerHTML = '';
574
+ const emptyMsg = noOutputMsg.cloneNode(true);
575
+ emptyMsg.style.display = 'block';
576
+ container.appendChild(emptyMsg);
577
+ return;
578
+ }
579
+
580
+ noOutputMsg.style.display = 'none';
581
+ container.innerHTML = '';
582
+ this.streamingParts.clear();
583
+
584
+ outputs.forEach(entry => this.renderOutputEntry(entry, entry.streaming));
585
+ }
586
+
587
+ renderOutputEntry(entry, isStreaming = false) {
588
+ const container = document.getElementById('agentOutputContainer');
589
+ const noOutputMsg = document.getElementById('noOutputMessage');
590
+ if (noOutputMsg) noOutputMsg.style.display = 'none';
591
+
592
+ const el = document.createElement('div');
593
+ el.className = `output-entry ${entry.type}`;
594
+ el.dataset.entryId = entry.id || Date.now();
595
+
596
+ const timeStr = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
597
+
598
+ let html = `<div class="output-meta"><span>${this.getEntryTypeLabel(entry.type)}</span><span>${timeStr}</span></div>`;
599
+
600
+ if (entry.type.startsWith('tool-')) {
601
+ html += this.renderToolEntry(entry);
602
+ } else if (entry.type === 'step-finish') {
603
+ html += `<div class="output-content">${this.escapeHtml(entry.content)}</div>`;
604
+ if (entry.tokens) {
605
+ html += `<div class="tokens-badge">Tokens: ${entry.tokens.input || 0} in / ${entry.tokens.output || 0} out</div>`;
606
+ }
607
+ if (entry.cost) {
608
+ html += `<span class="tokens-badge cost-badge">Cost: $${entry.cost.toFixed(4)}</span>`;
609
+ }
610
+ } else if (entry.type === 'command') {
611
+ html += `<div class="output-content">${this.escapeHtml(entry.content)}</div>`;
612
+ if (entry.output) {
613
+ html += `<div class="tool-output"><pre>${this.escapeHtml(entry.output)}</pre></div>`;
614
+ }
615
+ if (entry.exitCode !== undefined) {
616
+ html += `<div class="tokens-badge">Exit code: ${entry.exitCode}</div>`;
617
+ }
618
+ } else {
619
+ const contentClass = isStreaming ? 'output-content streaming' : 'output-content';
620
+ html += `<div class="${contentClass}" data-part-id="${entry.id || ''}">${this.escapeHtml(entry.content || '')}</div>`;
621
+ }
622
+
623
+ el.innerHTML = html;
624
+ container.appendChild(el);
625
+
626
+ // Track streaming elements
627
+ if (isStreaming && entry.id) {
628
+ this.streamingParts.set(entry.id, el);
629
+ }
630
+
631
+ // Auto-scroll
632
+ if (this.autoScroll) {
633
+ container.scrollTop = container.scrollHeight;
634
+ }
635
+ }
636
+
637
+ renderToolEntry(entry) {
638
+ let html = `<div class="tool-title">🔧 ${entry.toolName || 'Unknown Tool'}</div>`;
639
+ html += `<div class="output-content">${entry.title || entry.status}</div>`;
640
+
641
+ if (entry.input && Object.keys(entry.input).length > 0) {
642
+ html += `<div class="tool-input"><strong>Input:</strong><pre>${this.formatJson(entry.input)}</pre></div>`;
643
+ }
644
+
645
+ if (entry.output) {
646
+ html += `<div class="tool-output"><strong>Output:</strong><pre>${this.escapeHtml(this.truncateString(entry.output, 2000))}</pre></div>`;
647
+ }
648
+
649
+ if (entry.error) {
650
+ html += `<div class="tool-output error"><strong>Error:</strong><pre>${this.escapeHtml(entry.error)}</pre></div>`;
651
+ }
652
+
653
+ return html;
654
+ }
655
+
656
+ updateStreamingElement(partId, content, isStreaming = true) {
657
+ const el = this.streamingParts.get(partId);
658
+ if (!el) return;
659
+
660
+ const contentEl = el.querySelector(`[data-part-id="${partId}"]`);
661
+ if (contentEl) {
662
+ contentEl.textContent = content;
663
+ if (!isStreaming) {
664
+ contentEl.classList.remove('streaming');
665
+ }
666
+ }
667
+
668
+ // Auto-scroll
669
+ if (this.autoScroll) {
670
+ const container = document.getElementById('agentOutputContainer');
671
+ container.scrollTop = container.scrollHeight;
672
+ }
673
+ }
674
+
675
+ updateToolElement(partId, entry) {
676
+ const container = document.getElementById('agentOutputContainer');
677
+ const el = container.querySelector(`[data-entry-id="${partId}"]`);
678
+ if (!el) return;
679
+
680
+ el.className = `output-entry ${entry.type}`;
681
+
682
+ const timeStr = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '';
683
+ let html = `<div class="output-meta"><span>${this.getEntryTypeLabel(entry.type)}</span><span>${timeStr}</span></div>`;
684
+ html += this.renderToolEntry(entry);
685
+
686
+ el.innerHTML = html;
687
+ }
688
+
689
+ getEntryTypeLabel(type) {
690
+ const labels = {
691
+ 'text': '💬 Text',
692
+ 'reasoning': '🧠 Reasoning',
693
+ 'tool-pending': '⏳ Tool Pending',
694
+ 'tool-running': '🔄 Tool Running',
695
+ 'tool-completed': '✅ Tool Completed',
696
+ 'tool-error': '❌ Tool Error',
697
+ 'step-start': '▶️ Step Start',
698
+ 'step-finish': '⏹️ Step Finish',
699
+ 'file-edit': '📝 File Edit',
700
+ 'command': '💻 Command',
701
+ 'status': 'ℹ️ Status',
702
+ 'diagnostics': '🔍 Diagnostics',
703
+ 'error': '❌ Error'
704
+ };
705
+ return labels[type] || type;
706
+ }
707
+
708
+ async loadAgentMessages(codebaseId) {
709
+ try {
710
+ const serverUrl = this.getServerUrl();
711
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/messages?limit=50`);
712
+ if (response.ok) {
713
+ const data = await response.json();
714
+ // Process historical messages
715
+ if (data.messages && data.messages.length > 0) {
716
+ this.processHistoricalMessages(codebaseId, data.messages);
717
+ }
718
+ }
719
+ } catch (error) {
720
+ console.error('Failed to load agent messages:', error);
721
+ }
722
+ }
723
+
724
+ processHistoricalMessages(codebaseId, messages) {
725
+ // Process messages and their parts
726
+ messages.forEach(msg => {
727
+ if (msg.parts) {
728
+ msg.parts.forEach(part => {
729
+ if (part.type === 'text') {
730
+ this.addOutputEntry(codebaseId, {
731
+ id: part.id,
732
+ type: 'text',
733
+ content: part.text,
734
+ timestamp: msg.info?.time?.created ? new Date(msg.info.time.created * 1000).toISOString() : new Date().toISOString()
735
+ });
736
+ } else if (part.type === 'tool') {
737
+ const status = part.state?.status || 'pending';
738
+ this.addOutputEntry(codebaseId, {
739
+ id: part.id,
740
+ type: `tool-${status}`,
741
+ toolName: part.tool,
742
+ callId: part.callID,
743
+ input: part.state?.input,
744
+ output: part.state?.output,
745
+ error: part.state?.error,
746
+ title: part.state?.title,
747
+ status: status,
748
+ timestamp: new Date().toISOString()
749
+ });
750
+ }
751
+ });
752
+ }
753
+ });
754
+
755
+ // Re-display if current agent
756
+ if (codebaseId === this.currentOutputAgent) {
757
+ this.displayAgentOutput(codebaseId);
758
+ }
759
+ }
760
+
761
+ truncateString(str, maxLen) {
762
+ if (!str) return '';
763
+ if (str.length <= maxLen) return str;
764
+ return str.substring(0, maxLen) + '\n... (truncated)';
765
+ }
766
+
767
+ formatJson(obj) {
768
+ try {
769
+ return JSON.stringify(obj, null, 2);
770
+ } catch {
771
+ return String(obj);
772
+ }
773
+ }
774
+
775
+ toggleAutoScroll() {
776
+ this.autoScroll = !this.autoScroll;
777
+ const btn = document.getElementById('btnAutoScroll');
778
+ if (btn) {
779
+ btn.classList.toggle('active', this.autoScroll);
780
+ }
781
+ }
782
+
783
+ clearAgentOutput() {
784
+ if (this.currentOutputAgent) {
785
+ this.agentOutputs.set(this.currentOutputAgent, []);
786
+ this.displayAgentOutput(this.currentOutputAgent);
787
+ }
788
+ }
789
+
790
+ downloadAgentOutput() {
791
+ if (!this.currentOutputAgent) return;
792
+
793
+ const outputs = this.agentOutputs.get(this.currentOutputAgent) || [];
794
+ const codebase = this.codebases.find(cb => cb.id === this.currentOutputAgent);
795
+ const filename = `agent-output-${codebase?.name || this.currentOutputAgent}-${new Date().toISOString().slice(0, 10)}.json`;
796
+
797
+ const blob = new Blob([JSON.stringify(outputs, null, 2)], { type: 'application/json' });
798
+ const url = URL.createObjectURL(blob);
799
+ const a = document.createElement('a');
800
+ a.href = url;
801
+ a.download = filename;
802
+ a.click();
803
+ URL.revokeObjectURL(url);
804
+ }
805
+
806
+ disconnectAgentEventStream(codebaseId) {
807
+ const eventSource = this.agentOutputStreams.get(codebaseId);
808
+ if (eventSource) {
809
+ eventSource.close();
810
+ this.agentOutputStreams.delete(codebaseId);
811
+ }
812
+ }
813
+
814
+ // ========================================
815
+ // OpenCode Integration
816
+ // ========================================
817
+
818
+ async initOpenCode() {
819
+ await this.checkOpenCodeStatus();
820
+ await this.loadCodebases();
821
+ this.setupOpenCodeEventListeners();
822
+ // Poll codebases every 10 seconds
823
+ setInterval(() => this.loadCodebases(), 10000);
824
+ }
825
+
826
+ async checkOpenCodeStatus() {
827
+ try {
828
+ const serverUrl = this.getServerUrl();
829
+ const response = await fetch(`${serverUrl}/v1/opencode/status`);
830
+ const status = await response.json();
831
+
832
+ const statusEl = document.getElementById('opencodeStatus');
833
+ if (status.available) {
834
+ statusEl.innerHTML = `✅ OpenCode ready | Binary: <code>${status.opencode_binary}</code>`;
835
+ statusEl.style.background = '#d4edda';
836
+ } else {
837
+ statusEl.innerHTML = `⚠️ ${status.message}`;
838
+ statusEl.style.background = '#fff3cd';
839
+ }
840
+ } catch (error) {
841
+ console.error('Failed to check OpenCode status:', error);
842
+ const statusEl = document.getElementById('opencodeStatus');
843
+ statusEl.innerHTML = '❌ OpenCode integration unavailable';
844
+ statusEl.style.background = '#f8d7da';
845
+ }
846
+ }
847
+
848
+ async loadCodebases() {
849
+ try {
850
+ const serverUrl = this.getServerUrl();
851
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases`);
852
+ if (response.ok) {
853
+ this.codebases = await response.json();
854
+ this.displayCodebases();
855
+ this.updateAgentOutputSelector(); // Update agent output selector
856
+ }
857
+ } catch (error) {
858
+ console.error('Failed to load codebases:', error);
859
+ }
860
+ }
861
+
862
+ displayCodebases() {
863
+ const container = document.getElementById('codebasesContainer');
864
+ const noCodebasesMsg = document.getElementById('noCodebasesMessage');
865
+
866
+ if (this.codebases.length === 0) {
867
+ noCodebasesMsg.style.display = 'block';
868
+ return;
869
+ }
870
+
871
+ noCodebasesMsg.style.display = 'none';
872
+
873
+ // Keep existing codebases, update or add new ones
874
+ const existingIds = new Set();
875
+ this.codebases.forEach(cb => {
876
+ existingIds.add(cb.id);
877
+ let el = container.querySelector(`[data-codebase-id="${cb.id}"]`);
878
+
879
+ if (!el) {
880
+ el = this.createCodebaseElement(cb);
881
+ container.appendChild(el);
882
+ } else {
883
+ this.updateCodebaseElement(el, cb);
884
+ }
885
+ });
886
+
887
+ // Remove codebases that no longer exist
888
+ container.querySelectorAll('.codebase-item').forEach(el => {
889
+ if (!existingIds.has(el.dataset.codebaseId)) {
890
+ el.remove();
891
+ }
892
+ });
893
+ }
894
+
895
+ createCodebaseElement(codebase) {
896
+ const el = document.createElement('div');
897
+ const isWatching = codebase.status === 'watching';
898
+ el.className = `codebase-item ${codebase.status}${isWatching ? ' watching' : ''}`;
899
+ el.dataset.codebaseId = codebase.id;
900
+
901
+ // Get pending tasks for this codebase
902
+ const pendingTasks = this.tasks.filter(t => t.codebase_id === codebase.id && t.status === 'pending').length;
903
+ const workingTasks = this.tasks.filter(t => t.codebase_id === codebase.id && t.status === 'working').length;
904
+
905
+ el.innerHTML = `
906
+ <div class="codebase-header">
907
+ <div>
908
+ <div class="codebase-name">${this.escapeHtml(codebase.name)}</div>
909
+ <div class="codebase-path">${this.escapeHtml(codebase.path)}</div>
910
+ ${codebase.description ? `<div style="font-size: 0.9em; color: #6c757d; margin-top: 5px;">${this.escapeHtml(codebase.description)}</div>` : ''}
911
+ </div>
912
+ <span class="codebase-status ${codebase.status}${isWatching ? ' watching' : ''}">${isWatching ? '👁️ watching' : codebase.status}</span>
913
+ </div>
914
+
915
+ <div class="codebase-tasks">
916
+ ${pendingTasks > 0 ? `<span class="task-badge pending">⏳ ${pendingTasks} pending</span>` : ''}
917
+ ${workingTasks > 0 ? `<span class="task-badge working">🔄 ${workingTasks} working</span>` : ''}
918
+ </div>
919
+
920
+ <div class="watch-controls">
921
+ ${isWatching ? `
922
+ <div class="watch-indicator active">
923
+ <span class="watch-dot"></span>
924
+ <span>Watching for tasks...</span>
925
+ </div>
926
+ <button class="btn-secondary btn-small" onclick="stopWatchMode('${codebase.id}')">⏹️ Stop Watch</button>
927
+ ` : `
928
+ <button class="btn-secondary btn-small" onclick="startWatchMode('${codebase.id}')" title="Start watch mode to auto-process queued tasks">👁️ Start Watch Mode</button>
929
+ `}
930
+ </div>
931
+
932
+ <div class="codebase-actions">
933
+ <button class="btn-primary" onclick="monitor.showTriggerForm('${codebase.id}')">🎯 Trigger Agent</button>
934
+ <button class="btn-secondary" onclick="openTaskModal('${codebase.id}')">📋 Add Task</button>
935
+ <button class="btn-secondary" onclick="openAgentOutputModal('${codebase.id}')">👁️ Output</button>
936
+ <button class="btn-secondary" onclick="monitor.viewCodebaseStatus('${codebase.id}')">📊 Status</button>
937
+ <button class="btn-secondary" onclick="monitor.unregisterCodebase('${codebase.id}')" style="color: #dc3545;">🗑️</button>
938
+ </div>
939
+
940
+ <div class="trigger-form" id="trigger-form-${codebase.id}">
941
+ <textarea id="trigger-prompt-${codebase.id}" placeholder="Enter your prompt for the AI agent..."></textarea>
942
+ <select id="trigger-agent-${codebase.id}">
943
+ <option value="build">🔧 Build (Full access agent)</option>
944
+ <option value="plan">📋 Plan (Read-only analysis)</option>
945
+ <option value="general">🔄 General (Multi-step tasks)</option>
946
+ <option value="explore">🔍 Explore (Codebase search)</option>
947
+ </select>
948
+ <select id="trigger-model-${codebase.id}" class="model-selector">
949
+ <option value="">🤖 Default Model</option>
950
+ ${this.renderModelOptions()}
951
+ </select>
952
+ <div style="display: flex; gap: 10px;">
953
+ <button class="btn-primary" onclick="monitor.triggerAgent('${codebase.id}')" style="flex: 1;">🚀 Start Agent</button>
954
+ <button class="btn-secondary" onclick="monitor.hideTriggerForm('${codebase.id}')" style="flex: 1;">Cancel</button>
955
+ </div>
956
+ </div>
957
+ `;
958
+
959
+ return el;
960
+ }
961
+
962
+ updateCodebaseElement(el, codebase) {
963
+ // Update status badge
964
+ const statusEl = el.querySelector('.codebase-status');
965
+ statusEl.textContent = codebase.status;
966
+ statusEl.className = `codebase-status ${codebase.status}`;
967
+
968
+ // Update item class for styling
969
+ el.className = `codebase-item ${codebase.status}`;
970
+ }
971
+
972
+ showTriggerForm(codebaseId) {
973
+ const form = document.getElementById(`trigger-form-${codebaseId}`);
974
+ if (form) {
975
+ form.classList.add('show');
976
+ document.getElementById(`trigger-prompt-${codebaseId}`).focus();
977
+ }
978
+ }
979
+
980
+ hideTriggerForm(codebaseId) {
981
+ const form = document.getElementById(`trigger-form-${codebaseId}`);
982
+ if (form) {
983
+ form.classList.remove('show');
984
+ }
985
+ }
986
+
987
+ async triggerAgent(codebaseId) {
988
+ const prompt = document.getElementById(`trigger-prompt-${codebaseId}`).value;
989
+ const agent = document.getElementById(`trigger-agent-${codebaseId}`).value;
990
+ const model = document.getElementById(`trigger-model-${codebaseId}`).value;
991
+
992
+ if (!prompt.trim()) {
993
+ this.showToast('Please enter a prompt', 'error');
994
+ return;
995
+ }
996
+
997
+ try {
998
+ const serverUrl = this.getServerUrl();
999
+ const payload = {
1000
+ prompt: prompt,
1001
+ agent: agent,
1002
+ };
1003
+ if (model) {
1004
+ payload.model = model;
1005
+ }
1006
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/trigger`, {
1007
+ method: 'POST',
1008
+ headers: { 'Content-Type': 'application/json' },
1009
+ body: JSON.stringify(payload)
1010
+ });
1011
+
1012
+ const result = await response.json();
1013
+
1014
+ if (result.success) {
1015
+ this.showToast(`Agent '${agent}' triggered successfully!`, 'success');
1016
+ this.hideTriggerForm(codebaseId);
1017
+ document.getElementById(`trigger-prompt-${codebaseId}`).value = '';
1018
+ await this.loadCodebases();
1019
+ } else {
1020
+ this.showToast(`Failed: ${result.error}`, 'error');
1021
+ }
1022
+ } catch (error) {
1023
+ console.error('Failed to trigger agent:', error);
1024
+ this.showToast('Failed to trigger agent', 'error');
1025
+ }
1026
+ }
1027
+
1028
+ async viewCodebaseStatus(codebaseId) {
1029
+ try {
1030
+ const serverUrl = this.getServerUrl();
1031
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/status`);
1032
+ const status = await response.json();
1033
+
1034
+ const modal = document.createElement('div');
1035
+ modal.className = 'task-detail-modal show';
1036
+ modal.innerHTML = `
1037
+ <div class="modal-content">
1038
+ <div class="modal-header">
1039
+ <h2>${this.escapeHtml(status.name)}</h2>
1040
+ <button class="close-modal" onclick="this.closest('.task-detail-modal').remove()">×</button>
1041
+ </div>
1042
+ <div class="codebase-status ${status.status}" style="margin-bottom: 15px;">${status.status}</div>
1043
+
1044
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
1045
+ <div><strong>Path:</strong> <code>${this.escapeHtml(status.path)}</code></div>
1046
+ <div><strong>Registered:</strong> ${new Date(status.registered_at).toLocaleString()}</div>
1047
+ ${status.last_triggered ? `<div><strong>Last Triggered:</strong> ${new Date(status.last_triggered).toLocaleString()}</div>` : ''}
1048
+ ${status.session_id ? `<div><strong>Session ID:</strong> <code>${status.session_id}</code></div>` : ''}
1049
+ ${status.opencode_port ? `<div><strong>OpenCode Port:</strong> ${status.opencode_port}</div>` : ''}
1050
+ </div>
1051
+
1052
+ ${status.recent_messages ? `
1053
+ <h3>Recent Messages</h3>
1054
+ <div style="max-height: 300px; overflow-y: auto;">
1055
+ ${status.recent_messages.map(msg => `
1056
+ <div style="padding: 10px; margin: 5px 0; background: #e9ecef; border-radius: 8px;">
1057
+ ${this.escapeHtml(msg.content || JSON.stringify(msg))}
1058
+ </div>
1059
+ `).join('')}
1060
+ </div>
1061
+ ` : ''}
1062
+
1063
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
1064
+ ${status.status === 'busy' ? `
1065
+ <button class="btn-secondary" onclick="monitor.interruptAgent('${codebaseId}'); this.closest('.task-detail-modal').remove();">
1066
+ ⏹️ Interrupt
1067
+ </button>
1068
+ ` : ''}
1069
+ ${status.status === 'running' || status.status === 'busy' ? `
1070
+ <button class="btn-secondary" onclick="monitor.stopAgent('${codebaseId}'); this.closest('.task-detail-modal').remove();">
1071
+ 🛑 Stop Agent
1072
+ </button>
1073
+ ` : ''}
1074
+ </div>
1075
+ </div>
1076
+ `;
1077
+ document.body.appendChild(modal);
1078
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
1079
+ } catch (error) {
1080
+ console.error('Failed to get codebase status:', error);
1081
+ this.showToast('Failed to get status', 'error');
1082
+ }
1083
+ }
1084
+
1085
+ async interruptAgent(codebaseId) {
1086
+ try {
1087
+ const serverUrl = this.getServerUrl();
1088
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/interrupt`, {
1089
+ method: 'POST'
1090
+ });
1091
+ const result = await response.json();
1092
+
1093
+ if (result.success) {
1094
+ this.showToast('Agent interrupted', 'success');
1095
+ await this.loadCodebases();
1096
+ } else {
1097
+ this.showToast('Failed to interrupt agent', 'error');
1098
+ }
1099
+ } catch (error) {
1100
+ console.error('Failed to interrupt agent:', error);
1101
+ this.showToast('Failed to interrupt agent', 'error');
1102
+ }
1103
+ }
1104
+
1105
+ async stopAgent(codebaseId) {
1106
+ try {
1107
+ const serverUrl = this.getServerUrl();
1108
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/stop`, {
1109
+ method: 'POST'
1110
+ });
1111
+ const result = await response.json();
1112
+
1113
+ if (result.success) {
1114
+ this.showToast('Agent stopped', 'success');
1115
+ await this.loadCodebases();
1116
+ } else {
1117
+ this.showToast('Failed to stop agent', 'error');
1118
+ }
1119
+ } catch (error) {
1120
+ console.error('Failed to stop agent:', error);
1121
+ this.showToast('Failed to stop agent', 'error');
1122
+ }
1123
+ }
1124
+
1125
+ async unregisterCodebase(codebaseId) {
1126
+ if (!confirm('Are you sure you want to unregister this codebase?')) {
1127
+ return;
1128
+ }
1129
+
1130
+ try {
1131
+ const serverUrl = this.getServerUrl();
1132
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}`, {
1133
+ method: 'DELETE'
1134
+ });
1135
+ const result = await response.json();
1136
+
1137
+ if (result.success) {
1138
+ this.showToast('Codebase unregistered', 'success');
1139
+ await this.loadCodebases();
1140
+ } else {
1141
+ this.showToast('Failed to unregister', 'error');
1142
+ }
1143
+ } catch (error) {
1144
+ console.error('Failed to unregister codebase:', error);
1145
+ this.showToast('Failed to unregister', 'error');
1146
+ }
1147
+ }
1148
+
1149
+ setupOpenCodeEventListeners() {
1150
+ // Register codebase modal
1151
+ window.openRegisterModal = () => {
1152
+ document.getElementById('registerCodebaseModal').classList.add('show');
1153
+ document.getElementById('codebaseName').focus();
1154
+ };
1155
+
1156
+ window.closeRegisterModal = () => {
1157
+ document.getElementById('registerCodebaseModal').classList.remove('show');
1158
+ };
1159
+
1160
+ window.registerCodebase = async (event) => {
1161
+ event.preventDefault();
1162
+
1163
+ const name = document.getElementById('codebaseName').value;
1164
+ const path = document.getElementById('codebasePath').value;
1165
+ const description = document.getElementById('codebaseDescription').value;
1166
+
1167
+ try {
1168
+ const serverUrl = this.getServerUrl();
1169
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases`, {
1170
+ method: 'POST',
1171
+ headers: { 'Content-Type': 'application/json' },
1172
+ body: JSON.stringify({ name, path, description })
1173
+ });
1174
+
1175
+ const result = await response.json();
1176
+
1177
+ if (result.success) {
1178
+ this.showToast('Codebase registered!', 'success');
1179
+ closeRegisterModal();
1180
+ document.getElementById('codebaseName').value = '';
1181
+ document.getElementById('codebasePath').value = '';
1182
+ document.getElementById('codebaseDescription').value = '';
1183
+ await this.loadCodebases();
1184
+ } else {
1185
+ this.showToast(`Failed: ${result.detail || 'Unknown error'}`, 'error');
1186
+ }
1187
+ } catch (error) {
1188
+ console.error('Failed to register codebase:', error);
1189
+ this.showToast('Failed to register codebase', 'error');
1190
+ }
1191
+ };
1192
+
1193
+ // Close modal on outside click
1194
+ document.getElementById('registerCodebaseModal').onclick = (e) => {
1195
+ if (e.target.id === 'registerCodebaseModal') {
1196
+ closeRegisterModal();
1197
+ }
1198
+ };
1199
+ }
1200
+
1201
+ // ========================================
1202
+ // End OpenCode Integration
1203
+ // ========================================
1204
+
1205
+ async fetchTotalMessageCount() {
1206
+ try {
1207
+ const serverUrl = this.getServerUrl();
1208
+ const response = await fetch(`${serverUrl}/v1/monitor/messages/count`);
1209
+ if (response.ok) {
1210
+ const data = await response.json();
1211
+ this.totalStoredMessages = data.total;
1212
+ document.getElementById('totalStoredMessages').textContent = this.formatNumber(data.total);
1213
+ }
1214
+ } catch (error) {
1215
+ console.error('Failed to fetch total message count:', error);
1216
+ }
1217
+
1218
+ // Refresh count every 30 seconds
1219
+ setTimeout(() => this.fetchTotalMessageCount(), 30000);
1220
+ }
1221
+
1222
+ formatNumber(num) {
1223
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
1224
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
1225
+ return num.toString();
1226
+ }
1227
+
1228
+ connectToServer() {
1229
+ const serverUrl = this.getServerUrl();
1230
+
1231
+ // Close existing connection if any
1232
+ if (this.eventSource) {
1233
+ this.eventSource.close();
1234
+ }
1235
+
1236
+ console.log('Connecting to A2A server:', serverUrl);
1237
+
1238
+ // Connect to SSE endpoint for real-time updates
1239
+ this.eventSource = new EventSource(`${serverUrl}/v1/monitor/stream`);
1240
+
1241
+ this.eventSource.onopen = () => {
1242
+ console.log('✓ Connected to A2A server - Real-time monitoring active');
1243
+ this.updateConnectionStatus(true);
1244
+ this.showToast('Connected to A2A server', 'success');
1245
+ };
1246
+
1247
+ this.eventSource.onerror = (error) => {
1248
+ console.error('SSE connection error:', error);
1249
+ this.updateConnectionStatus(false);
1250
+
1251
+ // EventSource will automatically reconnect, but we'll add a fallback
1252
+ if (this.eventSource.readyState === EventSource.CLOSED) {
1253
+ console.log('Connection closed, reconnecting in 3 seconds...');
1254
+ setTimeout(() => this.connectToServer(), 3000);
1255
+ }
1256
+ };
1257
+
1258
+ this.eventSource.addEventListener('message', (event) => {
1259
+ const data = JSON.parse(event.data);
1260
+ this.handleMessage(data);
1261
+ });
1262
+
1263
+ this.eventSource.addEventListener('agent_status', (event) => {
1264
+ const data = JSON.parse(event.data);
1265
+ this.updateAgentStatus(data);
1266
+ });
1267
+
1268
+ this.eventSource.addEventListener('stats', (event) => {
1269
+ const data = JSON.parse(event.data);
1270
+ this.updateStats(data);
1271
+ });
1272
+
1273
+ // Keep connection alive with periodic heartbeat
1274
+ this.startHeartbeat();
1275
+
1276
+ // Also poll for agent list
1277
+ this.pollAgentList();
1278
+ }
1279
+
1280
+ async pollTaskQueue() {
1281
+ // Don't poll if paused
1282
+ if (this.isPaused) {
1283
+ setTimeout(() => this.pollTaskQueue(), 5000);
1284
+ return;
1285
+ }
1286
+
1287
+ try {
1288
+ // For MCP endpoints, we need to try port 9000 if not on the same port
1289
+ let mcpServerUrl = this.getServerUrl();
1290
+ const currentPort = window.location.port;
1291
+
1292
+ // If we're on the main A2A server port, try MCP server on 9000
1293
+ if (currentPort === '8000' || currentPort === '') {
1294
+ mcpServerUrl = mcpServerUrl.replace(':8000', ':9000');
1295
+ }
1296
+
1297
+ const response = await fetch(`${mcpServerUrl}/mcp/v1/tasks`);
1298
+ if (response.ok) {
1299
+ const data = await response.json();
1300
+ this.tasks = data.tasks || [];
1301
+ this.displayTasks();
1302
+ } else {
1303
+ console.warn(`Failed to fetch tasks from ${mcpServerUrl}/mcp/v1/tasks (status: ${response.status})`);
1304
+ }
1305
+ } catch (error) {
1306
+ console.error('Failed to fetch task queue:', error);
1307
+ }
1308
+
1309
+ // Poll every 5 seconds for task updates
1310
+ setTimeout(() => this.pollTaskQueue(), 5000);
1311
+ }
1312
+
1313
+ displayTasks() {
1314
+ const container = document.getElementById('tasksContainer');
1315
+
1316
+ // Filter tasks based on current filter
1317
+ let filteredTasks = this.tasks.map(task => this.normalizeTask(task));
1318
+ if (this.currentTaskFilter !== 'all') {
1319
+ filteredTasks = filteredTasks.filter(task => task.status === this.currentTaskFilter);
1320
+ }
1321
+
1322
+ if (filteredTasks.length === 0) {
1323
+ container.innerHTML = `
1324
+ <div class="empty-state">
1325
+ <div class="empty-state-icon">📭</div>
1326
+ <p>No tasks in queue</p>
1327
+ </div>
1328
+ `;
1329
+ return;
1330
+ }
1331
+
1332
+ container.innerHTML = '';
1333
+ filteredTasks.forEach(task => {
1334
+ const taskEl = this.createTaskElement(task);
1335
+ container.appendChild(taskEl);
1336
+ });
1337
+ }
1338
+
1339
+ createTaskElement(task) {
1340
+ const normalizedTask = this.normalizeTask(task);
1341
+ const taskEl = document.createElement('div');
1342
+ taskEl.className = `task-item ${normalizedTask.status}`;
1343
+ taskEl.dataset.taskId = normalizedTask.id;
1344
+ taskEl.onclick = () => this.showTaskDetails(normalizedTask);
1345
+
1346
+ const createdTime = normalizedTask.created_at ? new Date(normalizedTask.created_at).toLocaleString() : 'Unknown';
1347
+ const rawDescription = normalizedTask.description ? String(normalizedTask.description) : '';
1348
+ const description = rawDescription.length > 0 ?
1349
+ (rawDescription.length > 100 ? rawDescription.substring(0, 100) + '...' : rawDescription)
1350
+ : 'No description';
1351
+ const taskIdDisplay = normalizedTask.id ? `${String(normalizedTask.id).substring(0, 8)}...` : 'Unknown';
1352
+
1353
+ const priorityClass = normalizedTask.priority > 10 ? 'priority-urgent' : (normalizedTask.priority > 5 ? 'priority-high' : (normalizedTask.priority > 0 ? 'priority-normal' : 'priority-low'));
1354
+ const priorityLabel = normalizedTask.priority > 10 ? '🔴 Urgent' : (normalizedTask.priority > 5 ? '🟠 High' : (normalizedTask.priority > 0 ? '🟡 Normal' : '🟢 Low'));
1355
+ const priorityBadge = normalizedTask.priority >= 0 ? `<span class="task-badge ${priorityClass}">${priorityLabel}</span>` : '';
1356
+ const agentBadge = normalizedTask.agent_type ? `<span class="task-badge agent-type">🤖 ${normalizedTask.agent_type}</span>` : '';
1357
+ const codebaseDisplay = normalizedTask.codebase_id && normalizedTask.codebase_id !== 'global' ? `<span class="task-badge codebase">📁 ${String(normalizedTask.codebase_id).substring(0, 16)}</span>` : '';
1358
+
1359
+ taskEl.innerHTML = `
1360
+ <div class="task-header">
1361
+ <div>
1362
+ <div class="task-title">${this.escapeHtml(normalizedTask.title)}</div>
1363
+ <div class="task-badges">${priorityBadge}${agentBadge}${codebaseDisplay}</div>
1364
+ </div>
1365
+ <span class="task-status ${normalizedTask.status}">${normalizedTask.status}</span>
1366
+ </div>
1367
+ <div class="task-description">${this.escapeHtml(description)}</div>
1368
+ <div class="task-meta">
1369
+ <span>🆔 ${taskIdDisplay}</span>
1370
+ <span>⏰ ${createdTime}</span>
1371
+ </div>
1372
+ `;
1373
+
1374
+ return taskEl;
1375
+ }
1376
+
1377
+ showTaskDetails(task) {
1378
+ const normalizedTask = this.normalizeTask(task);
1379
+ const taskIdFull = normalizedTask.id ? String(normalizedTask.id) : 'Unknown';
1380
+ const modal = document.createElement('div');
1381
+ modal.className = 'task-detail-modal show';
1382
+ modal.innerHTML = `
1383
+ <div class="modal-content">
1384
+ <div class="modal-header">
1385
+ <h2>${this.escapeHtml(normalizedTask.title)}</h2>
1386
+ <button class="close-modal" onclick="this.closest('.task-detail-modal').remove()">×</button>
1387
+ </div>
1388
+ <div class="task-status ${normalizedTask.status}">${normalizedTask.status}</div>
1389
+ <div style="margin-top: 20px;">
1390
+ <h3 style="margin-bottom: 10px;">Description:</h3>
1391
+ <div style="white-space: pre-wrap; line-height: 1.6; color: #495057;">
1392
+ ${this.escapeHtml(normalizedTask.description || 'No description provided')}
1393
+ </div>
1394
+ </div>
1395
+ <div style="margin-top: 20px;">
1396
+ <h3 style="margin-bottom: 10px;">Details:</h3>
1397
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 0.9em;">
1398
+ <div><strong>Task ID:</strong> ${taskIdFull}</div>
1399
+ <div><strong>Status:</strong> ${normalizedTask.status}</div>
1400
+ <div><strong>Created:</strong> ${normalizedTask.created_at ? new Date(normalizedTask.created_at).toLocaleString() : 'Unknown'}</div>
1401
+ <div><strong>Updated:</strong> ${normalizedTask.updated_at ? new Date(normalizedTask.updated_at).toLocaleString() : 'Unknown'}</div>
1402
+ <div><strong>Agent Type:</strong> ${normalizedTask.agent_type || 'build'}</div>
1403
+ <div><strong>Priority:</strong> ${normalizedTask.priority || 0}</div>
1404
+ ${normalizedTask.codebase_id ? `<div><strong>Codebase ID:</strong> ${this.escapeHtml(String(normalizedTask.codebase_id))}</div>` : ''}
1405
+ </div>
1406
+ </div>
1407
+ <div class="task-actions" style="margin-top: 20px; display: flex; gap: 10px;">
1408
+ ${normalizedTask.status === 'pending' ? `
1409
+ <button class="btn-primary" onclick="monitor.updateTaskStatus('${normalizedTask.id}', 'working')">
1410
+ ▶️ Start Working
1411
+ </button>
1412
+ ` : ''}
1413
+ ${normalizedTask.status === 'working' ? `
1414
+ <button class="btn-primary" onclick="monitor.updateTaskStatus('${normalizedTask.id}', 'completed')">
1415
+ ✅ Mark Complete
1416
+ </button>
1417
+ <button class="btn-secondary" onclick="monitor.updateTaskStatus('${normalizedTask.id}', 'failed')">
1418
+ ❌ Mark Failed
1419
+ </button>
1420
+ ` : ''}
1421
+ ${normalizedTask.status !== 'cancelled' ? `
1422
+ <button class="btn-secondary" onclick="monitor.updateTaskStatus('${normalizedTask.id}', 'cancelled')">
1423
+ 🚫 Cancel Task
1424
+ </button>
1425
+ ` : ''}
1426
+ <button class="btn-secondary" onclick="navigator.clipboard.writeText('${normalizedTask.id}'); monitor.showToast('Task ID copied!', 'success')">
1427
+ 📋 Copy ID
1428
+ </button>
1429
+ </div>
1430
+ </div>
1431
+ `;
1432
+ document.body.appendChild(modal);
1433
+
1434
+ // Close on outside click
1435
+ modal.onclick = (e) => {
1436
+ if (e.target === modal) {
1437
+ modal.remove();
1438
+ }
1439
+ };
1440
+ }
1441
+
1442
+ async updateTaskStatus(taskId, newStatus) {
1443
+ try {
1444
+ // For MCP endpoints, we need to try port 9000 if not on the same port
1445
+ let mcpServerUrl = this.getServerUrl();
1446
+ const currentPort = window.location.port;
1447
+
1448
+ // If we're on the main A2A server port, try MCP server on 9000
1449
+ if (currentPort === '8000' || currentPort === '') {
1450
+ mcpServerUrl = mcpServerUrl.replace(':8000', ':9000');
1451
+ }
1452
+
1453
+ const response = await fetch(`${mcpServerUrl}/mcp/v1/tasks/${taskId}`, {
1454
+ method: 'PUT',
1455
+ headers: {
1456
+ 'Content-Type': 'application/json'
1457
+ },
1458
+ body: JSON.stringify({
1459
+ status: newStatus
1460
+ })
1461
+ });
1462
+
1463
+ if (response.ok) {
1464
+ this.showToast(`Task status updated to ${newStatus}`, 'success');
1465
+ // Close modal
1466
+ document.querySelector('.task-detail-modal')?.remove();
1467
+ // Refresh tasks
1468
+ await this.pollTaskQueue();
1469
+ } else {
1470
+ throw new Error('Failed to update task status');
1471
+ }
1472
+ } catch (error) {
1473
+ console.error('Error updating task status:', error);
1474
+ this.showToast('Failed to update task status', 'error');
1475
+ }
1476
+ }
1477
+
1478
+ normalizeTask(task) {
1479
+ if (!task || typeof task !== 'object') {
1480
+ return {
1481
+ id: 'unknown',
1482
+ title: 'Untitled Task',
1483
+ description: '',
1484
+ status: 'pending',
1485
+ created_at: null,
1486
+ updated_at: null,
1487
+ codebase_id: null,
1488
+ agent_type: 'build',
1489
+ priority: 0
1490
+ };
1491
+ }
1492
+
1493
+ const id = task.id || task.task_id || task.uuid || 'unknown';
1494
+ const title = task.title || task.name || 'Untitled Task';
1495
+ const description = task.description || task.details || '';
1496
+ const status = (task.status || task.state || 'pending').toString();
1497
+ const createdAt = task.created_at || task.createdAt || task.created || null;
1498
+ const updatedAt = task.updated_at || task.updatedAt || task.updated || createdAt;
1499
+ const codebaseId = task.codebase_id || task.codebaseId || null;
1500
+ const agentType = task.agent_type || task.agentType || 'build';
1501
+ const priority = task.priority || 0;
1502
+
1503
+ return {
1504
+ ...task,
1505
+ id: String(id),
1506
+ title: String(title),
1507
+ description: description !== null && description !== undefined ? String(description) : '',
1508
+ status,
1509
+ created_at: createdAt,
1510
+ updated_at: updatedAt,
1511
+ codebase_id: codebaseId,
1512
+ agent_type: agentType,
1513
+ priority: priority
1514
+ };
1515
+ }
1516
+
1517
+ startHeartbeat() {
1518
+ // Clear any existing heartbeat
1519
+ if (this.heartbeatInterval) {
1520
+ clearInterval(this.heartbeatInterval);
1521
+ }
1522
+
1523
+ // Check connection every 30 seconds
1524
+ this.heartbeatInterval = setInterval(() => {
1525
+ if (!this.eventSource || this.eventSource.readyState !== EventSource.OPEN) {
1526
+ console.log('Connection lost, reconnecting...');
1527
+ this.connectToServer();
1528
+ }
1529
+ }, 30000);
1530
+ }
1531
+
1532
+ getServerUrl() {
1533
+ // Try to get server URL from query params, current location, or use default
1534
+ const params = new URLSearchParams(window.location.search);
1535
+ if (params.get('server')) {
1536
+ return params.get('server');
1537
+ }
1538
+
1539
+ // If running on the same server, use relative URL
1540
+ if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
1541
+ return window.location.origin;
1542
+ }
1543
+
1544
+ // For local development, detect which port based on current page
1545
+ const currentPort = window.location.port;
1546
+ if (currentPort === '9000' || currentPort === '9001') {
1547
+ // If accessing monitor on MCP server port, use the same port
1548
+ return `http://localhost:${currentPort}`;
1549
+ } else {
1550
+ // Default to port 8000 for A2A server and port 9000 for MCP endpoints
1551
+ return 'http://localhost:8000';
1552
+ }
1553
+ }
1554
+
1555
+ async pollAgentList() {
1556
+ // Don't poll if paused
1557
+ if (this.isPaused) {
1558
+ setTimeout(() => this.pollAgentList(), 5000);
1559
+ return;
1560
+ }
1561
+
1562
+ try {
1563
+ const serverUrl = this.getServerUrl();
1564
+ const response = await fetch(`${serverUrl}/v1/monitor/agents`);
1565
+ if (response.ok) {
1566
+ const agents = await response.json();
1567
+ this.updateAgentList(agents);
1568
+ }
1569
+ } catch (error) {
1570
+ console.error('Failed to fetch agent list:', error);
1571
+ }
1572
+
1573
+ // Poll every 5 seconds for agent updates
1574
+ setTimeout(() => this.pollAgentList(), 5000);
1575
+ }
1576
+
1577
+ handleMessage(data) {
1578
+ if (this.isPaused) return;
1579
+
1580
+ const messageType = data.type === 'connected' ? 'system' : (data.type || 'agent');
1581
+ const agentName = data.agent_name || data.agentName || (data.type === 'connected' ? 'Monitoring Service' : 'Unknown');
1582
+ const content =
1583
+ data.content ??
1584
+ data.message ??
1585
+ (Array.isArray(data.parts) ? data.parts.map(part => part.text || part.content).join(' ') : undefined) ??
1586
+ (data.type === 'connected' ? 'Connected to monitoring stream' : '');
1587
+ const metadata = data.metadata || {};
1588
+
1589
+ const message = {
1590
+ id: Date.now() + Math.random(),
1591
+ timestamp: data.timestamp ? new Date(data.timestamp) : new Date(),
1592
+ type: messageType,
1593
+ agentName,
1594
+ content,
1595
+ metadata,
1596
+ ...data
1597
+ };
1598
+
1599
+ // Skip rendering if content is still undefined/null after fallback logic
1600
+ if (message.content === undefined || message.content === null) {
1601
+ message.content = '';
1602
+ }
1603
+
1604
+ this.messages.push(message);
1605
+ this.stats.totalMessages++;
1606
+
1607
+ // Track response times
1608
+ if (data.response_time) {
1609
+ this.stats.responseTimes.push(data.response_time);
1610
+ }
1611
+
1612
+ // Track tool calls
1613
+ if (data.type === 'tool') {
1614
+ this.stats.toolCalls++;
1615
+ }
1616
+
1617
+ // Track errors
1618
+ if (data.error || data.type === 'error') {
1619
+ this.stats.errors++;
1620
+ }
1621
+
1622
+ // Track tokens
1623
+ if (data.tokens) {
1624
+ this.stats.tokens += data.tokens;
1625
+ }
1626
+
1627
+ this.displayMessage(message);
1628
+ this.updateStatsDisplay();
1629
+ }
1630
+
1631
+ displayMessage(message) {
1632
+ const container = document.getElementById('messagesContainer');
1633
+
1634
+ // Check filter
1635
+ if (this.currentFilter !== 'all' && message.type !== this.currentFilter) {
1636
+ return;
1637
+ }
1638
+
1639
+ const messageEl = document.createElement('div');
1640
+ messageEl.className = `message ${message.type}`;
1641
+ messageEl.dataset.messageId = message.id;
1642
+ messageEl.dataset.messageType = message.type;
1643
+
1644
+ // Handle timestamp - ensure it's a Date object
1645
+ const timestamp = message.timestamp instanceof Date ? message.timestamp : new Date(message.timestamp);
1646
+ const timeStr = timestamp.toLocaleTimeString();
1647
+
1648
+ messageEl.innerHTML = `
1649
+ <div class="message-header">
1650
+ <div class="message-meta">
1651
+ <span class="agent-name">${this.escapeHtml(message.agentName)}</span>
1652
+ <span class="timestamp">${timeStr}</span>
1653
+ </div>
1654
+ <div class="message-actions">
1655
+ <button class="action-btn btn-flag" onclick="flagMessage('${message.id}')">🚩 Flag</button>
1656
+ <button class="action-btn btn-intervene" onclick="interveneAfterMessage('${message.id}')">✋ Intervene</button>
1657
+ <button class="action-btn btn-copy" onclick="copyMessage('${message.id}')">📋 Copy</button>
1658
+ </div>
1659
+ </div>
1660
+ <div class="message-content">${this.formatContent(message.content)}</div>
1661
+ ${this.formatMetadata(message.metadata)}
1662
+ `;
1663
+
1664
+ container.appendChild(messageEl);
1665
+
1666
+ // Auto-scroll to bottom
1667
+ container.scrollTop = container.scrollHeight;
1668
+
1669
+ // Keep only last 100 messages in DOM for performance
1670
+ while (container.children.length > 100) {
1671
+ container.removeChild(container.firstChild);
1672
+ }
1673
+ }
1674
+
1675
+ formatContent(content) {
1676
+ if (typeof content === 'object') {
1677
+ return `<pre>${JSON.stringify(content, null, 2)}</pre>`;
1678
+ }
1679
+ return this.escapeHtml(String(content));
1680
+ }
1681
+
1682
+ formatMetadata(metadata) {
1683
+ if (!metadata || Object.keys(metadata).length === 0) {
1684
+ return '';
1685
+ }
1686
+
1687
+ const items = Object.entries(metadata)
1688
+ .map(([key, value]) => `<strong>${key}:</strong> ${value}`)
1689
+ .join(' | ');
1690
+
1691
+ return `<div class="message-details">${items}</div>`;
1692
+ }
1693
+
1694
+ escapeHtml(text) {
1695
+ const div = document.createElement('div');
1696
+ div.textContent = text;
1697
+ return div.innerHTML;
1698
+ }
1699
+
1700
+ updateAgentStatus(data) {
1701
+ this.agents.set(data.agent_id, {
1702
+ name: data.name,
1703
+ status: data.status,
1704
+ lastSeen: new Date(),
1705
+ messagesCount: data.messages_count || 0
1706
+ });
1707
+
1708
+ this.updateAgentList();
1709
+ document.getElementById('activeAgents').textContent = this.agents.size;
1710
+ }
1711
+
1712
+ updateAgentList(agentData) {
1713
+ if (agentData) {
1714
+ agentData.forEach(agent => {
1715
+ this.agents.set(agent.id, {
1716
+ name: agent.name,
1717
+ status: agent.status,
1718
+ lastSeen: new Date(),
1719
+ messagesCount: agent.messages_count || 0
1720
+ });
1721
+ });
1722
+ }
1723
+
1724
+ const listEl = document.getElementById('agentList');
1725
+ const selectEl = document.getElementById('targetAgent');
1726
+
1727
+ listEl.innerHTML = '';
1728
+ selectEl.innerHTML = '<option value="">Select Agent...</option>';
1729
+
1730
+ this.agents.forEach((agent, id) => {
1731
+ // Add to list
1732
+ const li = document.createElement('li');
1733
+ li.className = 'agent-item';
1734
+ li.innerHTML = `
1735
+ <div class="agent-status">
1736
+ <span class="status-indicator ${agent.status}"></span>
1737
+ <span>${agent.name}</span>
1738
+ </div>
1739
+ <span>${agent.messagesCount} msgs</span>
1740
+ `;
1741
+ listEl.appendChild(li);
1742
+
1743
+ // Add to select
1744
+ const option = document.createElement('option');
1745
+ option.value = id;
1746
+ option.textContent = agent.name;
1747
+ selectEl.appendChild(option);
1748
+ });
1749
+ }
1750
+
1751
+ updateConnectionStatus(connected) {
1752
+ const statusEl = document.getElementById('connectionStatus');
1753
+ const indicator = document.querySelector('.status-indicator');
1754
+
1755
+ if (connected) {
1756
+ statusEl.textContent = 'Connected';
1757
+ indicator.className = 'status-indicator active';
1758
+ } else {
1759
+ statusEl.textContent = 'Disconnected';
1760
+ indicator.className = 'status-indicator idle';
1761
+ }
1762
+ }
1763
+
1764
+ updateStatsDisplay() {
1765
+ document.getElementById('messageCount').textContent = this.stats.totalMessages;
1766
+ document.getElementById('interventionCount').textContent = this.stats.interventions;
1767
+ document.getElementById('toolCalls').textContent = this.stats.toolCalls;
1768
+ document.getElementById('errorCount').textContent = this.stats.errors;
1769
+ document.getElementById('tokenCount').textContent = this.stats.tokens;
1770
+
1771
+ if (this.stats.responseTimes.length > 0) {
1772
+ const avg = this.stats.responseTimes.reduce((a, b) => a + b, 0) / this.stats.responseTimes.length;
1773
+ document.getElementById('avgResponseTime').textContent = Math.round(avg) + 'ms';
1774
+ }
1775
+ }
1776
+
1777
+ updateStats(data) {
1778
+ Object.assign(this.stats, data);
1779
+ this.updateStatsDisplay();
1780
+ }
1781
+
1782
+ async sendIntervention(event) {
1783
+ event.preventDefault();
1784
+
1785
+ const agentId = document.getElementById('targetAgent').value;
1786
+ const message = document.getElementById('interventionMessage').value;
1787
+
1788
+ if (!agentId || !message) {
1789
+ alert('Please select an agent and enter a message');
1790
+ return;
1791
+ }
1792
+
1793
+ try {
1794
+ const serverUrl = this.getServerUrl();
1795
+ const response = await fetch(`${serverUrl}/v1/monitor/intervene`, {
1796
+ method: 'POST',
1797
+ headers: {
1798
+ 'Content-Type': 'application/json'
1799
+ },
1800
+ body: JSON.stringify({
1801
+ agent_id: agentId,
1802
+ message: message,
1803
+ timestamp: new Date().toISOString()
1804
+ })
1805
+ });
1806
+
1807
+ if (response.ok) {
1808
+ this.stats.interventions++;
1809
+ this.updateStatsDisplay();
1810
+
1811
+ // Add intervention to messages
1812
+ this.handleMessage({
1813
+ type: 'human',
1814
+ agent_name: 'Human Operator',
1815
+ content: `Intervention to ${this.agents.get(agentId)?.name}: ${message}`,
1816
+ metadata: { intervention: true }
1817
+ });
1818
+
1819
+ // Clear form
1820
+ document.getElementById('interventionMessage').value = '';
1821
+
1822
+ this.showToast('Intervention sent successfully', 'success');
1823
+ } else {
1824
+ throw new Error('Failed to send intervention');
1825
+ }
1826
+ } catch (error) {
1827
+ console.error('Error sending intervention:', error);
1828
+ this.showToast('Failed to send intervention', 'error');
1829
+ }
1830
+ }
1831
+
1832
+ setupEventListeners() {
1833
+ // Filter messages
1834
+ window.filterMessages = (type) => {
1835
+ this.currentFilter = type;
1836
+
1837
+ // Update button states
1838
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1839
+ btn.classList.remove('active');
1840
+ });
1841
+ event.target.classList.add('active');
1842
+
1843
+ // Show/hide messages
1844
+ document.querySelectorAll('.message').forEach(msg => {
1845
+ if (type === 'all' || msg.dataset.messageType === type) {
1846
+ msg.style.display = 'block';
1847
+ } else {
1848
+ msg.style.display = 'none';
1849
+ }
1850
+ });
1851
+ };
1852
+
1853
+ // Search messages
1854
+ window.searchMessages = () => {
1855
+ const query = document.getElementById('searchInput').value.toLowerCase();
1856
+ document.querySelectorAll('.message').forEach(msg => {
1857
+ const content = msg.textContent.toLowerCase();
1858
+ msg.style.display = content.includes(query) ? 'block' : 'none';
1859
+ });
1860
+ };
1861
+
1862
+ // Send intervention
1863
+ window.sendIntervention = (event) => this.sendIntervention(event);
1864
+
1865
+ // Flag message
1866
+ window.flagMessage = (messageId) => {
1867
+ const message = this.messages.find(m => m.id == messageId);
1868
+ if (message) {
1869
+ message.flagged = true;
1870
+ this.showToast('Message flagged for review', 'info');
1871
+ }
1872
+ };
1873
+
1874
+ // Intervene after message
1875
+ window.interveneAfterMessage = (messageId) => {
1876
+ const message = this.messages.find(m => m.id == messageId);
1877
+ if (message) {
1878
+ document.getElementById('interventionMessage').value = `Regarding: "${message.content.substring(0, 50)}..."`;
1879
+ document.getElementById('interventionMessage').focus();
1880
+ }
1881
+ };
1882
+
1883
+ // Copy message
1884
+ window.copyMessage = (messageId) => {
1885
+ const message = this.messages.find(m => m.id == messageId);
1886
+ if (message) {
1887
+ navigator.clipboard.writeText(JSON.stringify(message, null, 2));
1888
+ this.showToast('Message copied to clipboard', 'success');
1889
+ }
1890
+ };
1891
+
1892
+ // Export functions
1893
+ window.exportJSON = () => this.exportData('json');
1894
+ window.exportCSV = () => this.exportData('csv');
1895
+ window.exportHTML = () => this.exportData('html');
1896
+ window.exportAllJSON = () => this.exportFromServer('json', true);
1897
+ window.exportAllCSV = () => this.exportFromServer('csv', true);
1898
+
1899
+ // Search persistent storage
1900
+ window.searchPersistent = () => this.searchPersistentMessages();
1901
+ window.closeSearchResults = () => this.closeSearchResults();
1902
+
1903
+ // Load historical messages
1904
+ window.loadHistoricalMessages = () => this.loadHistoricalMessages();
1905
+
1906
+ // Clear logs
1907
+ window.clearLogs = () => {
1908
+ if (confirm('Are you sure you want to clear all logs?')) {
1909
+ this.messages = [];
1910
+ document.getElementById('messagesContainer').innerHTML = '';
1911
+ this.showToast('Logs cleared', 'info');
1912
+ }
1913
+ };
1914
+
1915
+ // Pause monitoring
1916
+ window.pauseMonitoring = () => {
1917
+ this.isPaused = !this.isPaused;
1918
+ event.target.textContent = this.isPaused ? 'Resume' : 'Pause';
1919
+ this.showToast(this.isPaused ? 'Monitoring paused' : 'Monitoring resumed', 'info');
1920
+ };
1921
+
1922
+ // Task queue functions
1923
+ window.filterTasks = (status) => {
1924
+ this.currentTaskFilter = status;
1925
+
1926
+ // Update button states
1927
+ const buttons = document.querySelectorAll('.task-queue-panel .filter-btn');
1928
+ buttons.forEach(btn => btn.classList.remove('active'));
1929
+ event.target.classList.add('active');
1930
+
1931
+ this.displayTasks();
1932
+ };
1933
+
1934
+ window.refreshTasks = () => {
1935
+ this.pollTaskQueue();
1936
+ this.showToast('Tasks refreshed', 'info');
1937
+ };
1938
+
1939
+ window.createNewTask = async () => {
1940
+ const title = prompt('Enter task title:');
1941
+ if (!title) return;
1942
+
1943
+ const description = prompt('Enter task description (optional):');
1944
+
1945
+ const codebaseId = prompt('Enter codebase ID (optional, leave empty for global):');
1946
+ const agentType = prompt('Enter agent type (build, plan, general, explore):', 'build');
1947
+ const priorityStr = prompt('Enter priority (0-100, default 0):', '0');
1948
+ const priority = parseInt(priorityStr) || 0;
1949
+
1950
+ try {
1951
+ // For MCP endpoints, we need to try port 9000 if not on the same port
1952
+ let mcpServerUrl = this.getServerUrl();
1953
+ const currentPort = window.location.port;
1954
+
1955
+ // If we're on the main A2A server port, try MCP server on 9000
1956
+ if (currentPort === '8000' || currentPort === '') {
1957
+ mcpServerUrl = mcpServerUrl.replace(':8000', ':9000');
1958
+ }
1959
+
1960
+ const response = await fetch(`${mcpServerUrl}/mcp/v1/tasks`, {
1961
+ method: 'POST',
1962
+ headers: {
1963
+ 'Content-Type': 'application/json'
1964
+ },
1965
+ body: JSON.stringify({
1966
+ title: title,
1967
+ prompt: description || '',
1968
+ codebase_id: codebaseId || 'global',
1969
+ agent_type: agentType || 'build',
1970
+ priority: priority
1971
+ })
1972
+ });
1973
+
1974
+ if (response.ok) {
1975
+ const data = await response.json();
1976
+ this.showToast('Task created successfully!', 'success');
1977
+ await this.pollTaskQueue();
1978
+ } else {
1979
+ throw new Error('Failed to create task');
1980
+ }
1981
+ } catch (error) {
1982
+ console.error('Error creating task:', error);
1983
+ this.showToast('Failed to create task', 'error');
1984
+ }
1985
+ };
1986
+ }
1987
+
1988
+ exportData(format) {
1989
+ const data = this.messages.map(msg => ({
1990
+ timestamp: msg.timestamp.toISOString(),
1991
+ type: msg.type,
1992
+ agent: msg.agentName,
1993
+ content: msg.content,
1994
+ metadata: msg.metadata
1995
+ }));
1996
+
1997
+ let content, filename, mimeType;
1998
+
1999
+ if (format === 'json') {
2000
+ content = JSON.stringify(data, null, 2);
2001
+ filename = `a2a-logs-${Date.now()}.json`;
2002
+ mimeType = 'application/json';
2003
+ } else if (format === 'csv') {
2004
+ const headers = 'Timestamp,Type,Agent,Content\n';
2005
+ const rows = data.map(row =>
2006
+ `"${row.timestamp}","${row.type}","${row.agent}","${row.content}"`
2007
+ ).join('\n');
2008
+ content = headers + rows;
2009
+ filename = `a2a-logs-${Date.now()}.csv`;
2010
+ mimeType = 'text/csv';
2011
+ } else if (format === 'html') {
2012
+ content = this.generateHTMLReport(data);
2013
+ filename = `a2a-logs-${Date.now()}.html`;
2014
+ mimeType = 'text/html';
2015
+ }
2016
+
2017
+ const blob = new Blob([content], { type: mimeType });
2018
+ const url = URL.createObjectURL(blob);
2019
+ const a = document.createElement('a');
2020
+ a.href = url;
2021
+ a.download = filename;
2022
+ a.click();
2023
+ URL.revokeObjectURL(url);
2024
+
2025
+ this.showToast(`Exported as ${format.toUpperCase()}`, 'success');
2026
+ }
2027
+
2028
+ async exportFromServer(format, allMessages = false) {
2029
+ try {
2030
+ const serverUrl = this.getServerUrl();
2031
+ const endpoint = format === 'json' ? 'export/json' : 'export/csv';
2032
+ const url = `${serverUrl}/v1/monitor/${endpoint}?all_messages=${allMessages}`;
2033
+
2034
+ this.showToast(`Downloading ${allMessages ? 'all' : 'recent'} messages...`, 'info');
2035
+
2036
+ const response = await fetch(url);
2037
+ if (!response.ok) throw new Error('Export failed');
2038
+
2039
+ const blob = await response.blob();
2040
+ const downloadUrl = URL.createObjectURL(blob);
2041
+ const a = document.createElement('a');
2042
+ a.href = downloadUrl;
2043
+ a.download = `a2a-logs-${allMessages ? 'complete' : 'recent'}-${Date.now()}.${format}`;
2044
+ a.click();
2045
+ URL.revokeObjectURL(downloadUrl);
2046
+
2047
+ this.showToast(`Exported ${allMessages ? 'all' : 'recent'} messages as ${format.toUpperCase()}`, 'success');
2048
+ } catch (error) {
2049
+ console.error('Export failed:', error);
2050
+ this.showToast('Export failed', 'error');
2051
+ }
2052
+ }
2053
+
2054
+ async searchPersistentMessages() {
2055
+ const query = document.getElementById('searchInput').value.trim();
2056
+ if (!query) {
2057
+ this.showToast('Please enter a search term', 'info');
2058
+ return;
2059
+ }
2060
+
2061
+ try {
2062
+ const serverUrl = this.getServerUrl();
2063
+ const response = await fetch(`${serverUrl}/v1/monitor/messages/search?q=${encodeURIComponent(query)}&limit=100`);
2064
+
2065
+ if (!response.ok) throw new Error('Search failed');
2066
+
2067
+ const data = await response.json();
2068
+ this.displaySearchResults(data.results, query);
2069
+ } catch (error) {
2070
+ console.error('Search failed:', error);
2071
+ this.showToast('Search failed', 'error');
2072
+ }
2073
+ }
2074
+
2075
+ displaySearchResults(results, query) {
2076
+ const container = document.getElementById('searchResults');
2077
+ const content = document.getElementById('searchResultsContent');
2078
+
2079
+ container.style.display = 'block';
2080
+
2081
+ if (results.length === 0) {
2082
+ content.innerHTML = `<p>No results found for "${this.escapeHtml(query)}"</p>`;
2083
+ return;
2084
+ }
2085
+
2086
+ content.innerHTML = `<p>Found ${results.length} messages matching "${this.escapeHtml(query)}":</p>`;
2087
+
2088
+ results.forEach(msg => {
2089
+ const timestamp = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : 'Unknown';
2090
+ const resultEl = document.createElement('div');
2091
+ resultEl.className = 'search-result-item';
2092
+ resultEl.innerHTML = `
2093
+ <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
2094
+ <strong>${this.escapeHtml(msg.agent_name || 'Unknown')}</strong>
2095
+ <span style="color: #6c757d; font-size: 0.85em;">${timestamp}</span>
2096
+ </div>
2097
+ <div style="color: #495057;">${this.highlightQuery(msg.content || '', query)}</div>
2098
+ <div style="font-size: 0.8em; color: #6c757d; margin-top: 5px;">Type: ${msg.type || 'unknown'}</div>
2099
+ `;
2100
+ content.appendChild(resultEl);
2101
+ });
2102
+ }
2103
+
2104
+ highlightQuery(text, query) {
2105
+ if (!text || !query) return this.escapeHtml(text);
2106
+ const escaped = this.escapeHtml(text);
2107
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
2108
+ return escaped.replace(regex, '<mark style="background: #fff3cd;">$1</mark>');
2109
+ }
2110
+
2111
+ closeSearchResults() {
2112
+ document.getElementById('searchResults').style.display = 'none';
2113
+ }
2114
+
2115
+ async loadHistoricalMessages() {
2116
+ try {
2117
+ const serverUrl = this.getServerUrl();
2118
+ this.showToast('Loading historical messages...', 'info');
2119
+
2120
+ const response = await fetch(`${serverUrl}/v1/monitor/messages?limit=500&use_cache=false`);
2121
+ if (!response.ok) throw new Error('Failed to load history');
2122
+
2123
+ const messages = await response.json();
2124
+
2125
+ // Clear current messages and load historical
2126
+ document.getElementById('messagesContainer').innerHTML = '';
2127
+ this.messages = [];
2128
+
2129
+ // Add messages in chronological order
2130
+ messages.reverse().forEach(msg => {
2131
+ const message = {
2132
+ id: msg.id || Date.now() + Math.random(),
2133
+ timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
2134
+ type: msg.type || 'agent',
2135
+ agentName: msg.agent_name || 'Unknown',
2136
+ content: msg.content || '',
2137
+ metadata: msg.metadata || {}
2138
+ };
2139
+ this.messages.push(message);
2140
+ this.displayMessage(message);
2141
+ });
2142
+
2143
+ this.showToast(`Loaded ${messages.length} historical messages`, 'success');
2144
+ } catch (error) {
2145
+ console.error('Failed to load history:', error);
2146
+ this.showToast('Failed to load historical messages', 'error');
2147
+ }
2148
+ }
2149
+
2150
+ generateHTMLReport(data) {
2151
+ return `
2152
+ <!DOCTYPE html>
2153
+ <html>
2154
+ <head>
2155
+ <title>A2A Agent Logs - ${new Date().toISOString()}</title>
2156
+ <style>
2157
+ body { font-family: Arial, sans-serif; margin: 20px; }
2158
+ h1 { color: #667eea; }
2159
+ table { border-collapse: collapse; width: 100%; }
2160
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
2161
+ th { background-color: #667eea; color: white; }
2162
+ tr:nth-child(even) { background-color: #f2f2f2; }
2163
+ </style>
2164
+ </head>
2165
+ <body>
2166
+ <h1>A2A Agent Conversation Logs</h1>
2167
+ <p>Generated: ${new Date().toLocaleString()}</p>
2168
+ <p>Total Messages: ${data.length}</p>
2169
+ <table>
2170
+ <thead>
2171
+ <tr>
2172
+ <th>Timestamp</th>
2173
+ <th>Type</th>
2174
+ <th>Agent</th>
2175
+ <th>Content</th>
2176
+ </tr>
2177
+ </thead>
2178
+ <tbody>
2179
+ ${data.map(row => `
2180
+ <tr>
2181
+ <td>${row.timestamp}</td>
2182
+ <td>${row.type}</td>
2183
+ <td>${row.agent}</td>
2184
+ <td>${row.content}</td>
2185
+ </tr>
2186
+ `).join('')}
2187
+ </tbody>
2188
+ </table>
2189
+ </body>
2190
+ </html>
2191
+ `;
2192
+ }
2193
+
2194
+ showToast(message, type = 'info') {
2195
+ const toast = document.createElement('div');
2196
+ toast.className = 'toast';
2197
+ toast.textContent = message;
2198
+ toast.style.background = type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#17a2b8';
2199
+ document.body.appendChild(toast);
2200
+
2201
+ setTimeout(() => {
2202
+ toast.remove();
2203
+ }, 3000);
2204
+ }
2205
+
2206
+ startStatsUpdate() {
2207
+ setInterval(() => {
2208
+ this.updateStatsDisplay();
2209
+ }, 1000);
2210
+ }
2211
+ }
2212
+
2213
+ // Initialize monitor when page loads
2214
+ const monitor = new AgentMonitor();
2215
+
2216
+ // Global functions for agent output panel
2217
+ function toggleAutoScroll() {
2218
+ monitor.toggleAutoScroll();
2219
+ }
2220
+
2221
+ function clearAgentOutput() {
2222
+ monitor.clearAgentOutput();
2223
+ }
2224
+
2225
+ function downloadAgentOutput() {
2226
+ monitor.downloadAgentOutput();
2227
+ }
2228
+
2229
+ function switchAgentOutput() {
2230
+ monitor.switchAgentOutput();
2231
+ }
2232
+
2233
+ // ========================================
2234
+ // Task Management Functions
2235
+ // ========================================
2236
+
2237
+ function createNewTask() {
2238
+ openTaskModal();
2239
+ }
2240
+
2241
+ function openTaskModal(codebaseId = null) {
2242
+ const modal = document.getElementById('taskModal');
2243
+ const codebaseSelect = document.getElementById('taskCodebase');
2244
+
2245
+ // Populate codebase dropdown with registered codebases
2246
+ codebaseSelect.innerHTML = '<option value="global">Global (Any Worker)</option>';
2247
+ monitor.codebases.forEach(cb => {
2248
+ const option = document.createElement('option');
2249
+ option.value = cb.id;
2250
+ option.textContent = `${cb.name}`;
2251
+ if (cb.status === 'watching') {
2252
+ option.textContent += ' 👁️';
2253
+ }
2254
+ codebaseSelect.appendChild(option);
2255
+ });
2256
+
2257
+ // Pre-select codebase if provided
2258
+ if (codebaseId) {
2259
+ codebaseSelect.value = codebaseId;
2260
+ }
2261
+
2262
+ modal.style.display = 'flex';
2263
+ }
2264
+
2265
+ function closeTaskModal() {
2266
+ const modal = document.getElementById('taskModal');
2267
+ modal.style.display = 'none';
2268
+
2269
+ // Clear form
2270
+ document.getElementById('taskTitle').value = '';
2271
+ document.getElementById('taskDescription').value = '';
2272
+ document.getElementById('taskPriority').value = '2';
2273
+ document.getElementById('taskContext').value = '';
2274
+ document.getElementById('taskCodebase').value = 'global';
2275
+ document.getElementById('taskAgentType').value = 'general';
2276
+ }
2277
+
2278
+ async function submitTask(event) {
2279
+ event.preventDefault();
2280
+
2281
+ const codebaseId = document.getElementById('taskCodebase').value;
2282
+ const agentType = document.getElementById('taskAgentType').value;
2283
+ const title = document.getElementById('taskTitle').value;
2284
+ const description = document.getElementById('taskDescription').value;
2285
+ const priority = parseInt(document.getElementById('taskPriority').value);
2286
+ const context = document.getElementById('taskContext').value;
2287
+
2288
+ if (!title) {
2289
+ monitor.showToast('Please enter a task title', 'error');
2290
+ return;
2291
+ }
2292
+
2293
+ try {
2294
+ const serverUrl = monitor.getServerUrl();
2295
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/tasks`, {
2296
+ method: 'POST',
2297
+ headers: { 'Content-Type': 'application/json' },
2298
+ body: JSON.stringify({
2299
+ title: title,
2300
+ description: description,
2301
+ priority: priority,
2302
+ context: context || null,
2303
+ agent_type: agentType
2304
+ })
2305
+ });
2306
+
2307
+ if (!response.ok) {
2308
+ const error = await response.json();
2309
+ throw new Error(error.detail || 'Failed to create task');
2310
+ }
2311
+
2312
+ const task = await response.json();
2313
+ monitor.showToast(`Task "${title}" created successfully!`, 'success');
2314
+ closeTaskModal();
2315
+ refreshTasks();
2316
+
2317
+ } catch (error) {
2318
+ console.error('Failed to create task:', error);
2319
+ monitor.showToast(`Failed to create task: ${error.message}`, 'error');
2320
+ }
2321
+ }
2322
+
2323
+ async function refreshTasks() {
2324
+ try {
2325
+ const serverUrl = monitor.getServerUrl();
2326
+ const response = await fetch(`${serverUrl}/v1/opencode/tasks`);
2327
+
2328
+ if (!response.ok) throw new Error('Failed to fetch tasks');
2329
+
2330
+ const tasks = await response.json();
2331
+ monitor.tasks = tasks;
2332
+ displayTasks(tasks);
2333
+
2334
+ } catch (error) {
2335
+ console.error('Failed to refresh tasks:', error);
2336
+ }
2337
+ }
2338
+
2339
+ function filterTasks(status) {
2340
+ // Update active filter button
2341
+ document.querySelectorAll('.filter-btn').forEach(btn => {
2342
+ btn.classList.remove('active');
2343
+ if (btn.textContent.toLowerCase().includes(status) ||
2344
+ (status === 'all' && btn.textContent.toLowerCase().includes('all'))) {
2345
+ btn.classList.add('active');
2346
+ }
2347
+ });
2348
+
2349
+ monitor.currentTaskFilter = status;
2350
+
2351
+ // Filter and display tasks
2352
+ let filtered = monitor.tasks;
2353
+ if (status !== 'all') {
2354
+ filtered = monitor.tasks.filter(t => t.status === status);
2355
+ }
2356
+ displayTasks(filtered);
2357
+ }
2358
+
2359
+ function displayTasks(tasks) {
2360
+ const container = document.getElementById('tasksContainer');
2361
+ if (!container) return;
2362
+
2363
+ if (tasks.length === 0) {
2364
+ container.innerHTML = `
2365
+ <div class="empty-state" style="padding: 40px; text-align: center; color: #6c757d;">
2366
+ <p>📋 No tasks in queue</p>
2367
+ <p style="font-size: 0.9em;">Create a task to assign work to an agent</p>
2368
+ </div>
2369
+ `;
2370
+ return;
2371
+ }
2372
+
2373
+ container.innerHTML = tasks.map(task => {
2374
+ const codebase = monitor.codebases.find(cb => cb.id === task.codebase_id);
2375
+ const agentName = codebase ? codebase.name : 'Unknown Agent';
2376
+ const statusEmoji = getTaskStatusEmoji(task.status);
2377
+ const priorityEmoji = getPriorityEmoji(task.priority);
2378
+ const createdAt = new Date(task.created_at).toLocaleString();
2379
+
2380
+ return `
2381
+ <div class="task-item ${task.status}" data-task-id="${task.id}">
2382
+ <div class="task-header" style="display: flex; justify-content: space-between; align-items: center;">
2383
+ <span class="task-title">${statusEmoji} ${monitor.escapeHtml(task.title)}</span>
2384
+ <span class="task-priority">${priorityEmoji}</span>
2385
+ </div>
2386
+ <div class="task-meta">
2387
+ <span>🤖 ${monitor.escapeHtml(agentName)}</span>
2388
+ <span>⏰ ${createdAt}</span>
2389
+ </div>
2390
+ <div class="task-description" style="margin-top: 8px; font-size: 0.9em; color: #6c757d;">
2391
+ ${monitor.escapeHtml(task.description).substring(0, 100)}${task.description.length > 100 ? '...' : ''}
2392
+ </div>
2393
+ <div class="task-actions" style="margin-top: 10px; display: flex; gap: 8px;">
2394
+ ${task.status === 'pending' ? `
2395
+ <button class="btn-small btn-primary" onclick="startTask('${task.id}')">▶️ Start</button>
2396
+ <button class="btn-small btn-secondary" onclick="cancelTask('${task.id}')">❌ Cancel</button>
2397
+ ` : ''}
2398
+ ${task.status === 'working' ? `
2399
+ <button class="btn-small btn-secondary" onclick="viewTaskOutput('${task.id}')">👁️ View Output</button>
2400
+ <button class="btn-small btn-danger" onclick="cancelTask('${task.id}')">⏹️ Stop</button>
2401
+ ` : ''}
2402
+ ${task.status === 'completed' ? `
2403
+ <button class="btn-small btn-secondary" onclick="viewTaskResult('${task.id}')">📄 View Result</button>
2404
+ ` : ''}
2405
+ </div>
2406
+ </div>
2407
+ `;
2408
+ }).join('');
2409
+ }
2410
+
2411
+ function getTaskStatusEmoji(status) {
2412
+ const emojis = {
2413
+ 'pending': '⏳',
2414
+ 'working': '🔄',
2415
+ 'completed': '✅',
2416
+ 'failed': '❌',
2417
+ 'cancelled': '🚫'
2418
+ };
2419
+ return emojis[status] || '❓';
2420
+ }
2421
+
2422
+ function getPriorityEmoji(priority) {
2423
+ const emojis = {
2424
+ 1: '🟢 Low',
2425
+ 2: '🟡 Normal',
2426
+ 3: '🟠 High',
2427
+ 4: '🔴 Urgent'
2428
+ };
2429
+ return emojis[priority] || '🟡 Normal';
2430
+ }
2431
+
2432
+ async function cancelTask(taskId) {
2433
+ if (!confirm('Are you sure you want to cancel this task?')) return;
2434
+
2435
+ try {
2436
+ const serverUrl = monitor.getServerUrl();
2437
+ const response = await fetch(`${serverUrl}/v1/opencode/tasks/${taskId}/cancel`, {
2438
+ method: 'POST'
2439
+ });
2440
+
2441
+ if (!response.ok) throw new Error('Failed to cancel task');
2442
+
2443
+ monitor.showToast('Task cancelled', 'success');
2444
+ refreshTasks();
2445
+
2446
+ } catch (error) {
2447
+ console.error('Failed to cancel task:', error);
2448
+ monitor.showToast('Failed to cancel task', 'error');
2449
+ }
2450
+ }
2451
+
2452
+ async function startTask(taskId) {
2453
+ try {
2454
+ const serverUrl = monitor.getServerUrl();
2455
+ const task = monitor.tasks.find(t => t.id === taskId);
2456
+
2457
+ if (!task) {
2458
+ monitor.showToast('Task not found', 'error');
2459
+ return;
2460
+ }
2461
+
2462
+ // Find the codebase and trigger the agent
2463
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${task.codebase_id}/trigger`, {
2464
+ method: 'POST',
2465
+ headers: { 'Content-Type': 'application/json' },
2466
+ body: JSON.stringify({
2467
+ prompt: `Task: ${task.title}\n\nDescription: ${task.description}${task.context ? `\n\nContext: ${task.context}` : ''}`
2468
+ })
2469
+ });
2470
+
2471
+ if (!response.ok) throw new Error('Failed to start task');
2472
+
2473
+ monitor.showToast('Task started!', 'success');
2474
+
2475
+ // Open agent output modal
2476
+ openAgentOutputModal(task.codebase_id);
2477
+ refreshTasks();
2478
+
2479
+ } catch (error) {
2480
+ console.error('Failed to start task:', error);
2481
+ monitor.showToast('Failed to start task', 'error');
2482
+ }
2483
+ }
2484
+
2485
+ function viewTaskOutput(taskId) {
2486
+ const task = monitor.tasks.find(t => t.id === taskId);
2487
+ if (task) {
2488
+ openAgentOutputModal(task.codebase_id);
2489
+ }
2490
+ }
2491
+
2492
+ function viewTaskResult(taskId) {
2493
+ const task = monitor.tasks.find(t => t.id === taskId);
2494
+ if (task && task.result) {
2495
+ alert(`Task Result:\n\n${task.result}`);
2496
+ } else {
2497
+ monitor.showToast('No result available', 'info');
2498
+ }
2499
+ }
2500
+
2501
+ // ========================================
2502
+ // Watch Mode Functions
2503
+ // ========================================
2504
+
2505
+ async function startWatchMode(codebaseId) {
2506
+ try {
2507
+ const serverUrl = monitor.getServerUrl();
2508
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/watch/start`, {
2509
+ method: 'POST',
2510
+ headers: { 'Content-Type': 'application/json' },
2511
+ body: JSON.stringify({
2512
+ poll_interval: 5.0
2513
+ })
2514
+ });
2515
+
2516
+ if (!response.ok) {
2517
+ const error = await response.json();
2518
+ throw new Error(error.detail || 'Failed to start watch mode');
2519
+ }
2520
+
2521
+ monitor.showToast('Watch mode started! Agent will process queued tasks automatically.', 'success');
2522
+ monitor.loadCodebases(); // Refresh to show watching status
2523
+
2524
+ } catch (error) {
2525
+ console.error('Failed to start watch mode:', error);
2526
+ monitor.showToast(`Failed to start watch mode: ${error.message}`, 'error');
2527
+ }
2528
+ }
2529
+
2530
+ async function stopWatchMode(codebaseId) {
2531
+ try {
2532
+ const serverUrl = monitor.getServerUrl();
2533
+ const response = await fetch(`${serverUrl}/v1/opencode/codebases/${codebaseId}/watch/stop`, {
2534
+ method: 'POST'
2535
+ });
2536
+
2537
+ if (!response.ok) throw new Error('Failed to stop watch mode');
2538
+
2539
+ monitor.showToast('Watch mode stopped', 'success');
2540
+ monitor.loadCodebases(); // Refresh status
2541
+
2542
+ } catch (error) {
2543
+ console.error('Failed to stop watch mode:', error);
2544
+ monitor.showToast('Failed to stop watch mode', 'error');
2545
+ }
2546
+ }
2547
+
2548
+ // ========================================
2549
+ // Agent Output Modal Functions
2550
+ // ========================================
2551
+
2552
+ function openAgentOutputModal(codebaseId) {
2553
+ const modal = document.getElementById('agentOutputModal');
2554
+ const content = document.getElementById('agentOutputContent');
2555
+
2556
+ // Set current agent and display output
2557
+ monitor.currentOutputAgent = codebaseId;
2558
+
2559
+ // Initialize output array if needed
2560
+ if (!monitor.agentOutputs.has(codebaseId)) {
2561
+ monitor.agentOutputs.set(codebaseId, []);
2562
+ }
2563
+
2564
+ // Connect to event stream if not connected
2565
+ if (!monitor.agentOutputStreams.has(codebaseId)) {
2566
+ monitor.connectAgentEventStream(codebaseId);
2567
+ }
2568
+
2569
+ // Display existing output in modal
2570
+ const outputs = monitor.agentOutputs.get(codebaseId) || [];
2571
+ if (outputs.length === 0) {
2572
+ content.innerHTML = '<div class="loading">Waiting for agent output...</div>';
2573
+ } else {
2574
+ content.innerHTML = outputs.map(entry => formatOutputEntry(entry)).join('');
2575
+ }
2576
+
2577
+ modal.style.display = 'flex';
2578
+ }
2579
+
2580
+ function closeAgentOutputModal() {
2581
+ document.getElementById('agentOutputModal').style.display = 'none';
2582
+ }
2583
+
2584
+ function formatOutputEntry(entry) {
2585
+ const timestamp = new Date(entry.timestamp).toLocaleTimeString();
2586
+ let className = 'output-entry';
2587
+ let content = '';
2588
+
2589
+ switch (entry.type) {
2590
+ case 'thinking':
2591
+ className += ' thinking';
2592
+ content = `<span class="output-time">[${timestamp}]</span> 🧠 ${monitor.escapeHtml(entry.content)}`;
2593
+ break;
2594
+ case 'tool_call':
2595
+ className += ' tool-call';
2596
+ content = `<span class="output-time">[${timestamp}]</span> 🔧 Tool: ${monitor.escapeHtml(entry.tool_name || 'unknown')}`;
2597
+ if (entry.tool_args) {
2598
+ content += `<pre style="margin: 5px 0; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px; overflow-x: auto;">${monitor.escapeHtml(JSON.stringify(entry.tool_args, null, 2))}</pre>`;
2599
+ }
2600
+ break;
2601
+ case 'tool_result':
2602
+ className += ' tool-result';
2603
+ content = `<span class="output-time">[${timestamp}]</span> ✅ Result: <pre style="margin: 5px 0; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px; overflow-x: auto;">${monitor.escapeHtml(entry.content?.substring(0, 500) || '')}${entry.content?.length > 500 ? '...' : ''}</pre>`;
2604
+ break;
2605
+ case 'response':
2606
+ className += ' response';
2607
+ content = `<span class="output-time">[${timestamp}]</span> 💬 ${monitor.escapeHtml(entry.content)}`;
2608
+ break;
2609
+ case 'error':
2610
+ className += ' error';
2611
+ content = `<span class="output-time">[${timestamp}]</span> ❌ ${monitor.escapeHtml(entry.content)}`;
2612
+ break;
2613
+ default:
2614
+ content = `<span class="output-time">[${timestamp}]</span> ${monitor.escapeHtml(entry.content || '')}`;
2615
+ }
2616
+
2617
+ return `<div class="${className}" style="padding: 8px; margin-bottom: 4px; border-radius: 4px; background: rgba(255,255,255,0.05);">${content}</div>`;
2618
+ }
2619
+
2620
+ function exportAgentOutput() {
2621
+ if (!monitor.currentOutputAgent) return;
2622
+
2623
+ const outputs = monitor.agentOutputs.get(monitor.currentOutputAgent) || [];
2624
+ const codebase = monitor.codebases.find(cb => cb.id === monitor.currentOutputAgent);
2625
+ const name = codebase ? codebase.name : 'agent';
2626
+
2627
+ const blob = new Blob([JSON.stringify(outputs, null, 2)], { type: 'application/json' });
2628
+ const url = URL.createObjectURL(blob);
2629
+ const a = document.createElement('a');
2630
+ a.href = url;
2631
+ a.download = `${name}_output_${new Date().toISOString().slice(0, 10)}.json`;
2632
+ a.click();
2633
+ URL.revokeObjectURL(url);
2634
+ }
2635
+
2636
+ // Add keyboard shortcuts
2637
+ document.addEventListener('keydown', (e) => {
2638
+ // Escape to close modals
2639
+ if (e.key === 'Escape') {
2640
+ closeTaskModal();
2641
+ closeAgentOutputModal();
2642
+ closeRegisterModal();
2643
+ }
2644
+
2645
+ // Ctrl+N for new task
2646
+ if (e.ctrlKey && e.key === 'n') {
2647
+ e.preventDefault();
2648
+ openTaskModal();
2649
+ }
2650
+ });
2651
+
2652
+ // Poll tasks periodically
2653
+ setInterval(() => {
2654
+ if (!document.hidden) {
2655
+ refreshTasks();
2656
+ }
2657
+ }, 10000);
2658
+
2659
+ // Initial task load
2660
+ setTimeout(() => {
2661
+ refreshTasks();
2662
+ }, 1000);