agentgui 1.0.226 → 1.0.228
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.
- package/lib/claude-runner.js +54 -6
- package/package.json +1 -1
- package/server.js +157 -28
- package/static/index.html +1 -0
- package/static/js/client.js +49 -1
- package/static/js/conversations.js +23 -2
- package/static/js/voice.js +13 -0
package/lib/claude-runner.js
CHANGED
|
@@ -347,18 +347,28 @@ class AgentRunner {
|
|
|
347
347
|
|
|
348
348
|
if (message.id === promptId && message.result && message.result.stopReason) {
|
|
349
349
|
completed = true;
|
|
350
|
+
draining = true;
|
|
350
351
|
clearTimeout(timeoutHandle);
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
// Wait a short time for any remaining events to be flushed before killing
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
draining = false;
|
|
355
|
+
try { proc.kill(); } catch (e) {}
|
|
356
|
+
resolve({ outputs, sessionId });
|
|
357
|
+
}, 1000);
|
|
353
358
|
return;
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
if (message.id === promptId && message.error) {
|
|
357
|
-
originalHandler(message);
|
|
358
362
|
completed = true;
|
|
363
|
+
draining = true;
|
|
359
364
|
clearTimeout(timeoutHandle);
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
// Process the error message first, then delay for remaining events
|
|
366
|
+
originalHandler(message);
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
draining = false;
|
|
369
|
+
try { proc.kill(); } catch (e) {}
|
|
370
|
+
reject(new Error(message.error.message || 'ACP prompt error'));
|
|
371
|
+
}, 1000);
|
|
362
372
|
return;
|
|
363
373
|
}
|
|
364
374
|
|
|
@@ -367,8 +377,11 @@ class AgentRunner {
|
|
|
367
377
|
|
|
368
378
|
buffer = '';
|
|
369
379
|
proc.stdout.removeAllListeners('data');
|
|
380
|
+
let draining = false;
|
|
370
381
|
proc.stdout.on('data', (chunk) => {
|
|
371
|
-
if (timedOut
|
|
382
|
+
if (timedOut) return;
|
|
383
|
+
// Continue processing during drain period after stopReason/error
|
|
384
|
+
if (completed && !draining) return;
|
|
372
385
|
|
|
373
386
|
buffer += chunk.toString();
|
|
374
387
|
const lines = buffer.split('\n');
|
|
@@ -397,6 +410,19 @@ class AgentRunner {
|
|
|
397
410
|
clearTimeout(timeoutHandle);
|
|
398
411
|
if (timedOut || completed) return;
|
|
399
412
|
|
|
413
|
+
// Flush any remaining buffer content
|
|
414
|
+
if (buffer.trim()) {
|
|
415
|
+
try {
|
|
416
|
+
const message = JSON.parse(buffer.trim());
|
|
417
|
+
if (message.id === 1 && message.result) {
|
|
418
|
+
initialized = true;
|
|
419
|
+
}
|
|
420
|
+
enhancedHandler(message);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
// Buffer might be incomplete, ignore parse errors on close
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
400
426
|
if (code === 0 || outputs.length > 0) {
|
|
401
427
|
resolve({ outputs, sessionId });
|
|
402
428
|
} else {
|
|
@@ -889,6 +915,28 @@ registry.register({
|
|
|
889
915
|
protocolHandler: acpProtocolHandler
|
|
890
916
|
});
|
|
891
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Kilo CLI Agent (OpenCode fork)
|
|
920
|
+
* Built on OpenCode, supports ACP protocol
|
|
921
|
+
* Uses 'kilo' command - installed via npm install -g @kilocode/cli
|
|
922
|
+
*/
|
|
923
|
+
registry.register({
|
|
924
|
+
id: 'kilo',
|
|
925
|
+
name: 'Kilo CLI',
|
|
926
|
+
command: 'kilo',
|
|
927
|
+
protocol: 'acp',
|
|
928
|
+
supportsStdin: false,
|
|
929
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'],
|
|
930
|
+
|
|
931
|
+
buildArgs(prompt, config) {
|
|
932
|
+
return ['acp'];
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
protocolHandler(message, context) {
|
|
936
|
+
return acpProtocolHandler(message, context);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
892
940
|
/**
|
|
893
941
|
* Main export function - runs any registered agent
|
|
894
942
|
*/
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,6 +24,68 @@ async function ensurePocketTtsSetup(onProgress) {
|
|
|
24
24
|
return serverTTS.ensureInstalled(onProgress);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Model download manager
|
|
28
|
+
const modelDownloadState = {
|
|
29
|
+
downloading: false,
|
|
30
|
+
progress: null,
|
|
31
|
+
error: null,
|
|
32
|
+
complete: false
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function ensureModelsDownloaded() {
|
|
36
|
+
const { createRequire: cr } = await import('module');
|
|
37
|
+
const r = cr(import.meta.url);
|
|
38
|
+
const { checkAllFilesExist, downloadModels } = r('sttttsmodels');
|
|
39
|
+
|
|
40
|
+
if (checkAllFilesExist()) {
|
|
41
|
+
modelDownloadState.complete = true;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (modelDownloadState.downloading) {
|
|
46
|
+
// Wait for current download
|
|
47
|
+
while (modelDownloadState.downloading) {
|
|
48
|
+
await new Promise(r => setTimeout(r, 100));
|
|
49
|
+
}
|
|
50
|
+
return modelDownloadState.complete;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
modelDownloadState.downloading = true;
|
|
54
|
+
modelDownloadState.error = null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await downloadModels((progress) => {
|
|
58
|
+
modelDownloadState.progress = progress;
|
|
59
|
+
// Broadcast progress to all connected clients
|
|
60
|
+
broadcastSync({
|
|
61
|
+
type: 'model_download_progress',
|
|
62
|
+
progress: {
|
|
63
|
+
started: progress.started,
|
|
64
|
+
done: progress.done,
|
|
65
|
+
error: progress.error,
|
|
66
|
+
downloading: progress.downloading,
|
|
67
|
+
type: progress.type,
|
|
68
|
+
completedFiles: progress.completedFiles,
|
|
69
|
+
totalFiles: progress.totalFiles,
|
|
70
|
+
totalDownloaded: progress.totalDownloaded,
|
|
71
|
+
totalBytes: progress.totalBytes
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
modelDownloadState.complete = true;
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
modelDownloadState.error = err.message;
|
|
79
|
+
broadcastSync({
|
|
80
|
+
type: 'model_download_progress',
|
|
81
|
+
progress: { error: err.message, done: true }
|
|
82
|
+
});
|
|
83
|
+
return false;
|
|
84
|
+
} finally {
|
|
85
|
+
modelDownloadState.downloading = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
27
89
|
function eagerTTS(text, conversationId, sessionId) {
|
|
28
90
|
getSpeech().then(speech => {
|
|
29
91
|
const status = speech.getStatus();
|
|
@@ -204,39 +266,87 @@ const discoveredAgents = discoverAgents();
|
|
|
204
266
|
|
|
205
267
|
const modelCache = new Map();
|
|
206
268
|
|
|
269
|
+
const AGENT_MODEL_COMMANDS = {
|
|
270
|
+
'opencode': 'opencode models',
|
|
271
|
+
'kilo': 'kilo models',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const AGENT_DEFAULT_MODELS = {
|
|
275
|
+
'claude-code': [
|
|
276
|
+
{ id: '', label: 'Default' },
|
|
277
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
278
|
+
{ id: 'opus', label: 'Opus' },
|
|
279
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
280
|
+
{ id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5' },
|
|
281
|
+
{ id: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
282
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' }
|
|
283
|
+
],
|
|
284
|
+
'gemini': [
|
|
285
|
+
{ id: '', label: 'Default' },
|
|
286
|
+
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
287
|
+
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
288
|
+
{ id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }
|
|
289
|
+
],
|
|
290
|
+
'goose': [
|
|
291
|
+
{ id: '', label: 'Default' },
|
|
292
|
+
{ id: 'claude-sonnet-4-5', label: 'Sonnet 4.5' },
|
|
293
|
+
{ id: 'claude-opus-4-5', label: 'Opus 4.5' }
|
|
294
|
+
],
|
|
295
|
+
'codex': [
|
|
296
|
+
{ id: '', label: 'Default' },
|
|
297
|
+
{ id: 'o4-mini', label: 'o4-mini' },
|
|
298
|
+
{ id: 'o3', label: 'o3' },
|
|
299
|
+
{ id: 'o3-mini', label: 'o3-mini' }
|
|
300
|
+
]
|
|
301
|
+
};
|
|
302
|
+
|
|
207
303
|
async function getModelsForAgent(agentId) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
];
|
|
304
|
+
const cached = modelCache.get(agentId);
|
|
305
|
+
if (cached && Date.now() - cached.timestamp < 300000) {
|
|
306
|
+
return cached.models;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (AGENT_DEFAULT_MODELS[agentId]) {
|
|
310
|
+
const models = AGENT_DEFAULT_MODELS[agentId];
|
|
311
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
312
|
+
return models;
|
|
218
313
|
}
|
|
219
|
-
|
|
314
|
+
|
|
315
|
+
if (AGENT_MODEL_COMMANDS[agentId]) {
|
|
220
316
|
try {
|
|
221
|
-
const result = execSync(
|
|
317
|
+
const result = execSync(AGENT_MODEL_COMMANDS[agentId], { encoding: 'utf-8', timeout: 15000 });
|
|
222
318
|
const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
|
|
223
319
|
const models = [{ id: '', label: 'Default' }];
|
|
224
320
|
for (const line of lines) {
|
|
225
321
|
models.push({ id: line, label: line });
|
|
226
322
|
}
|
|
323
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
227
324
|
return models;
|
|
228
325
|
} catch (_) {
|
|
229
326
|
return [{ id: '', label: 'Default' }];
|
|
230
327
|
}
|
|
231
328
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
329
|
+
|
|
330
|
+
const { getRegisteredAgents } = await import('./lib/claude-runner.js');
|
|
331
|
+
const agents = getRegisteredAgents();
|
|
332
|
+
const agent = agents.find(a => a.id === agentId);
|
|
333
|
+
|
|
334
|
+
if (agent && agent.command) {
|
|
335
|
+
const modelCmd = `${agent.command} models`;
|
|
336
|
+
try {
|
|
337
|
+
const result = execSync(modelCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
338
|
+
const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
|
|
339
|
+
if (lines.length > 0) {
|
|
340
|
+
const models = [{ id: '', label: 'Default' }];
|
|
341
|
+
for (const line of lines) {
|
|
342
|
+
models.push({ id: line, label: line });
|
|
343
|
+
}
|
|
344
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
345
|
+
return models;
|
|
346
|
+
}
|
|
347
|
+
} catch (_) {}
|
|
239
348
|
}
|
|
349
|
+
|
|
240
350
|
return [];
|
|
241
351
|
}
|
|
242
352
|
|
|
@@ -754,7 +864,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
754
864
|
}
|
|
755
865
|
|
|
756
866
|
if (pathOnly === '/api/conversations' && req.method === 'GET') {
|
|
757
|
-
|
|
867
|
+
const conversations = queries.getConversationsList();
|
|
868
|
+
// Filter out stale streaming state for conversations not in activeExecutions
|
|
869
|
+
for (const conv of conversations) {
|
|
870
|
+
if (conv.isStreaming && !activeExecutions.has(conv.id)) {
|
|
871
|
+
conv.isStreaming = 0;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
sendJSON(req, res, 200, { conversations });
|
|
758
875
|
return;
|
|
759
876
|
}
|
|
760
877
|
|
|
@@ -1553,11 +1670,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1553
1670
|
pythonDetected: pyInfo.found,
|
|
1554
1671
|
pythonVersion: pyInfo.version || null,
|
|
1555
1672
|
setupMessage: baseStatus.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
|
|
1673
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
1674
|
+
modelsComplete: modelDownloadState.complete,
|
|
1675
|
+
modelsError: modelDownloadState.error,
|
|
1676
|
+
modelsProgress: modelDownloadState.progress,
|
|
1556
1677
|
});
|
|
1557
1678
|
} catch (err) {
|
|
1558
1679
|
sendJSON(req, res, 200, {
|
|
1559
1680
|
sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
|
|
1560
1681
|
setupMessage: 'Will setup on first TTS request',
|
|
1682
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
1683
|
+
modelsComplete: modelDownloadState.complete,
|
|
1684
|
+
modelsError: modelDownloadState.error,
|
|
1561
1685
|
});
|
|
1562
1686
|
}
|
|
1563
1687
|
return;
|
|
@@ -2484,14 +2608,9 @@ function performAgentHealthCheck() {
|
|
|
2484
2608
|
markAgentDead(conversationId, entry, 'Agent process died unexpectedly');
|
|
2485
2609
|
} else if (now - entry.lastActivity > STUCK_AGENT_THRESHOLD_MS) {
|
|
2486
2610
|
debugLog(`[HEALTH] Agent PID ${entry.pid} for conv ${conversationId} has no activity for ${Math.round((now - entry.lastActivity) / 1000)}s`);
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
conversationId,
|
|
2491
|
-
error: 'Agent may be stuck (no activity for 10 minutes)',
|
|
2492
|
-
recoverable: true,
|
|
2493
|
-
timestamp: now
|
|
2494
|
-
});
|
|
2611
|
+
// Kill stuck agent and clear streaming state
|
|
2612
|
+
try { process.kill(entry.pid, 'SIGTERM'); } catch (e) {}
|
|
2613
|
+
markAgentDead(conversationId, entry, 'Agent was stuck (no activity for 10 minutes)');
|
|
2495
2614
|
}
|
|
2496
2615
|
} else {
|
|
2497
2616
|
if (now - entry.startTime > NO_PID_GRACE_PERIOD_MS) {
|
|
@@ -2519,6 +2638,16 @@ function onServerReady() {
|
|
|
2519
2638
|
// Resume interrupted streams after recovery
|
|
2520
2639
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
2521
2640
|
|
|
2641
|
+
// Start model downloads in background after server is ready
|
|
2642
|
+
setTimeout(() => {
|
|
2643
|
+
ensureModelsDownloaded().then(ok => {
|
|
2644
|
+
if (ok) console.log('[MODELS] Speech models ready');
|
|
2645
|
+
else console.log('[MODELS] Speech model download failed');
|
|
2646
|
+
}).catch(err => {
|
|
2647
|
+
console.error('[MODELS] Download error:', err.message);
|
|
2648
|
+
});
|
|
2649
|
+
}, 2000);
|
|
2650
|
+
|
|
2522
2651
|
getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
|
|
2523
2652
|
|
|
2524
2653
|
performAutoImport();
|
package/static/index.html
CHANGED
|
@@ -2271,6 +2271,7 @@
|
|
|
2271
2271
|
.connection-dot.unknown { background: #6b7280; }
|
|
2272
2272
|
.connection-dot.disconnected { background: #ef4444; animation: pulse 1.5s ease-in-out infinite; }
|
|
2273
2273
|
.connection-dot.reconnecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
|
|
2274
|
+
.connection-dot.downloading { background: #3b82f6; animation: pulse 1s ease-in-out infinite; }
|
|
2274
2275
|
|
|
2275
2276
|
.connection-tooltip {
|
|
2276
2277
|
position: absolute;
|
package/static/js/client.js
CHANGED
|
@@ -432,6 +432,9 @@ class AgentGUIClient {
|
|
|
432
432
|
case 'rate_limit_clear':
|
|
433
433
|
this.handleRateLimitClear(data);
|
|
434
434
|
break;
|
|
435
|
+
case 'model_download_progress':
|
|
436
|
+
this._handleModelDownloadProgress(data.progress);
|
|
437
|
+
break;
|
|
435
438
|
default:
|
|
436
439
|
break;
|
|
437
440
|
}
|
|
@@ -813,6 +816,7 @@ class AgentGUIClient {
|
|
|
813
816
|
}
|
|
814
817
|
|
|
815
818
|
if (data.message.role === 'user') {
|
|
819
|
+
// Find pending message by matching content to avoid duplicates
|
|
816
820
|
const pending = outputEl.querySelector('.message-sending');
|
|
817
821
|
if (pending) {
|
|
818
822
|
pending.id = '';
|
|
@@ -826,6 +830,12 @@ class AgentGUIClient {
|
|
|
826
830
|
this.emit('message:created', data);
|
|
827
831
|
return;
|
|
828
832
|
}
|
|
833
|
+
// Check if a user message with this ID already exists (prevents duplicate on race condition)
|
|
834
|
+
const existingMsg = outputEl.querySelector(`[data-msg-id="${data.message.id}"]`);
|
|
835
|
+
if (existingMsg) {
|
|
836
|
+
this.emit('message:created', data);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
829
839
|
}
|
|
830
840
|
|
|
831
841
|
const messageHtml = `
|
|
@@ -1826,7 +1836,7 @@ class AgentGUIClient {
|
|
|
1826
1836
|
}
|
|
1827
1837
|
|
|
1828
1838
|
_updateConnectionIndicator(quality) {
|
|
1829
|
-
if (this._indicatorDebounce) return;
|
|
1839
|
+
if (this._indicatorDebounce && !this._modelDownloadInProgress) return;
|
|
1830
1840
|
this._indicatorDebounce = true;
|
|
1831
1841
|
setTimeout(() => { this._indicatorDebounce = false; }, 1000);
|
|
1832
1842
|
|
|
@@ -1848,6 +1858,21 @@ class AgentGUIClient {
|
|
|
1848
1858
|
const label = indicator.querySelector('.connection-label');
|
|
1849
1859
|
if (!dot || !label) return;
|
|
1850
1860
|
|
|
1861
|
+
// Check if model download is in progress
|
|
1862
|
+
if (this._modelDownloadInProgress) {
|
|
1863
|
+
dot.className = 'connection-dot downloading';
|
|
1864
|
+
const progress = this._modelDownloadProgress;
|
|
1865
|
+
if (progress && progress.totalBytes > 0) {
|
|
1866
|
+
const pct = Math.round((progress.totalDownloaded / progress.totalBytes) * 100);
|
|
1867
|
+
label.textContent = `Models ${pct}%`;
|
|
1868
|
+
} else if (progress && progress.downloading) {
|
|
1869
|
+
label.textContent = 'Downloading...';
|
|
1870
|
+
} else {
|
|
1871
|
+
label.textContent = 'Loading models...';
|
|
1872
|
+
}
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1851
1876
|
dot.className = 'connection-dot';
|
|
1852
1877
|
if (quality === 'disconnected' || quality === 'reconnecting') {
|
|
1853
1878
|
dot.classList.add(quality);
|
|
@@ -1859,6 +1884,29 @@ class AgentGUIClient {
|
|
|
1859
1884
|
}
|
|
1860
1885
|
}
|
|
1861
1886
|
|
|
1887
|
+
_handleModelDownloadProgress(progress) {
|
|
1888
|
+
this._modelDownloadProgress = progress;
|
|
1889
|
+
|
|
1890
|
+
if (progress.error) {
|
|
1891
|
+
this._modelDownloadInProgress = false;
|
|
1892
|
+
console.error('[Models] Download error:', progress.error);
|
|
1893
|
+
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if (progress.done) {
|
|
1898
|
+
this._modelDownloadInProgress = false;
|
|
1899
|
+
console.log('[Models] Download complete');
|
|
1900
|
+
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
if (progress.started || progress.downloading) {
|
|
1905
|
+
this._modelDownloadInProgress = true;
|
|
1906
|
+
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1862
1910
|
_toggleConnectionTooltip() {
|
|
1863
1911
|
let tooltip = document.getElementById('connection-tooltip');
|
|
1864
1912
|
if (tooltip) { tooltip.remove(); return; }
|
|
@@ -13,6 +13,7 @@ class ConversationManager {
|
|
|
13
13
|
this.newBtn = document.querySelector('[data-new-conversation]');
|
|
14
14
|
this.sidebarEl = document.querySelector('[data-sidebar]');
|
|
15
15
|
this.streamingConversations = new Set();
|
|
16
|
+
this.agents = new Map();
|
|
16
17
|
|
|
17
18
|
this.folderBrowser = {
|
|
18
19
|
modal: null,
|
|
@@ -32,6 +33,7 @@ class ConversationManager {
|
|
|
32
33
|
async init() {
|
|
33
34
|
this.newBtn?.addEventListener('click', () => this.openFolderBrowser());
|
|
34
35
|
this.setupDelegatedListeners();
|
|
36
|
+
await this.loadAgents();
|
|
35
37
|
this.loadConversations();
|
|
36
38
|
this.setupWebSocketListener();
|
|
37
39
|
this.setupFolderBrowser();
|
|
@@ -40,6 +42,25 @@ class ConversationManager {
|
|
|
40
42
|
setInterval(() => this.loadConversations(), 30000);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
async loadAgents() {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch((window.__BASE_URL || '') + '/api/agents');
|
|
48
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
for (const agent of data.agents || []) {
|
|
51
|
+
this.agents.set(agent.id, agent);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('[ConversationManager] Error loading agents:', err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getAgentDisplayName(agentId) {
|
|
59
|
+
if (!agentId) return 'Unknown';
|
|
60
|
+
const agent = this.agents.get(agentId);
|
|
61
|
+
return agent?.name || agentId;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
setupDelegatedListeners() {
|
|
44
65
|
this.listEl.addEventListener('click', (e) => {
|
|
45
66
|
const deleteBtn = e.target.closest('[data-delete-conv]');
|
|
@@ -375,7 +396,7 @@ class ConversationManager {
|
|
|
375
396
|
const isStreaming = this.streamingConversations.has(conv.id);
|
|
376
397
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
377
398
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
378
|
-
const agent = conv.agentType
|
|
399
|
+
const agent = this.getAgentDisplayName(conv.agentType);
|
|
379
400
|
const modelLabel = conv.model ? ` (${conv.model})` : '';
|
|
380
401
|
const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
|
|
381
402
|
const metaParts = [agent + modelLabel, timestamp];
|
|
@@ -403,7 +424,7 @@ class ConversationManager {
|
|
|
403
424
|
|
|
404
425
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
405
426
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
406
|
-
const agent = conv.agentType
|
|
427
|
+
const agent = this.getAgentDisplayName(conv.agentType);
|
|
407
428
|
const modelLabel = conv.model ? ` (${conv.model})` : '';
|
|
408
429
|
const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
|
|
409
430
|
const metaParts = [agent + modelLabel, timestamp];
|
package/static/js/voice.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
var spokenChunks = new Set();
|
|
16
16
|
var renderedSeqs = new Set();
|
|
17
17
|
var isLoadingHistory = false;
|
|
18
|
+
var _lastVoiceBlockText = null;
|
|
19
|
+
var _lastVoiceBlockTime = 0;
|
|
18
20
|
var selectedVoiceId = localStorage.getItem('voice-selected-id') || 'default';
|
|
19
21
|
var ttsAudioCache = new Map();
|
|
20
22
|
var TTS_CLIENT_CACHE_MAX = 50;
|
|
@@ -646,6 +648,14 @@
|
|
|
646
648
|
function handleVoiceBlock(block, isNew) {
|
|
647
649
|
if (!block || !block.type) return;
|
|
648
650
|
if (block.type === 'text' && block.text) {
|
|
651
|
+
// Deduplicate: prevent rendering the same text block twice within 500ms
|
|
652
|
+
var now = Date.now();
|
|
653
|
+
if (_lastVoiceBlockText === block.text && (now - _lastVoiceBlockTime) < 500) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
_lastVoiceBlockText = block.text;
|
|
657
|
+
_lastVoiceBlockTime = now;
|
|
658
|
+
|
|
649
659
|
var div = addVoiceBlock(block.text, false);
|
|
650
660
|
if (div && isNew && ttsEnabled) {
|
|
651
661
|
div.classList.add('speaking');
|
|
@@ -661,6 +671,9 @@
|
|
|
661
671
|
var container = document.getElementById('voiceMessages');
|
|
662
672
|
if (!container) return;
|
|
663
673
|
container.innerHTML = '';
|
|
674
|
+
// Reset dedup state when loading a new conversation
|
|
675
|
+
_lastVoiceBlockText = null;
|
|
676
|
+
_lastVoiceBlockTime = 0;
|
|
664
677
|
if (!conversationId) {
|
|
665
678
|
showVoiceEmpty(container);
|
|
666
679
|
return;
|