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.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- 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);
|