agentgui 1.0.226 → 1.0.227
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 +67 -19
- package/static/js/client.js +7 -0
- 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
|
@@ -204,39 +204,87 @@ const discoveredAgents = discoverAgents();
|
|
|
204
204
|
|
|
205
205
|
const modelCache = new Map();
|
|
206
206
|
|
|
207
|
+
const AGENT_MODEL_COMMANDS = {
|
|
208
|
+
'opencode': 'opencode models',
|
|
209
|
+
'kilo': 'kilo models',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const AGENT_DEFAULT_MODELS = {
|
|
213
|
+
'claude-code': [
|
|
214
|
+
{ id: '', label: 'Default' },
|
|
215
|
+
{ id: 'sonnet', label: 'Sonnet' },
|
|
216
|
+
{ id: 'opus', label: 'Opus' },
|
|
217
|
+
{ id: 'haiku', label: 'Haiku' },
|
|
218
|
+
{ id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5' },
|
|
219
|
+
{ id: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
220
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' }
|
|
221
|
+
],
|
|
222
|
+
'gemini': [
|
|
223
|
+
{ id: '', label: 'Default' },
|
|
224
|
+
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
225
|
+
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
226
|
+
{ id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }
|
|
227
|
+
],
|
|
228
|
+
'goose': [
|
|
229
|
+
{ id: '', label: 'Default' },
|
|
230
|
+
{ id: 'claude-sonnet-4-5', label: 'Sonnet 4.5' },
|
|
231
|
+
{ id: 'claude-opus-4-5', label: 'Opus 4.5' }
|
|
232
|
+
],
|
|
233
|
+
'codex': [
|
|
234
|
+
{ id: '', label: 'Default' },
|
|
235
|
+
{ id: 'o4-mini', label: 'o4-mini' },
|
|
236
|
+
{ id: 'o3', label: 'o3' },
|
|
237
|
+
{ id: 'o3-mini', label: 'o3-mini' }
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
|
|
207
241
|
async function getModelsForAgent(agentId) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{ id: 'sonnet', label: 'Sonnet' },
|
|
212
|
-
{ id: 'opus', label: 'Opus' },
|
|
213
|
-
{ id: 'haiku', label: 'Haiku' },
|
|
214
|
-
{ id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5' },
|
|
215
|
-
{ id: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
216
|
-
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' }
|
|
217
|
-
];
|
|
242
|
+
const cached = modelCache.get(agentId);
|
|
243
|
+
if (cached && Date.now() - cached.timestamp < 300000) {
|
|
244
|
+
return cached.models;
|
|
218
245
|
}
|
|
219
|
-
|
|
246
|
+
|
|
247
|
+
if (AGENT_DEFAULT_MODELS[agentId]) {
|
|
248
|
+
const models = AGENT_DEFAULT_MODELS[agentId];
|
|
249
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
250
|
+
return models;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (AGENT_MODEL_COMMANDS[agentId]) {
|
|
220
254
|
try {
|
|
221
|
-
const result = execSync(
|
|
255
|
+
const result = execSync(AGENT_MODEL_COMMANDS[agentId], { encoding: 'utf-8', timeout: 15000 });
|
|
222
256
|
const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
|
|
223
257
|
const models = [{ id: '', label: 'Default' }];
|
|
224
258
|
for (const line of lines) {
|
|
225
259
|
models.push({ id: line, label: line });
|
|
226
260
|
}
|
|
261
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
227
262
|
return models;
|
|
228
263
|
} catch (_) {
|
|
229
264
|
return [{ id: '', label: 'Default' }];
|
|
230
265
|
}
|
|
231
266
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
267
|
+
|
|
268
|
+
const { getRegisteredAgents } = await import('./lib/claude-runner.js');
|
|
269
|
+
const agents = getRegisteredAgents();
|
|
270
|
+
const agent = agents.find(a => a.id === agentId);
|
|
271
|
+
|
|
272
|
+
if (agent && agent.command) {
|
|
273
|
+
const modelCmd = `${agent.command} models`;
|
|
274
|
+
try {
|
|
275
|
+
const result = execSync(modelCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
276
|
+
const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
|
|
277
|
+
if (lines.length > 0) {
|
|
278
|
+
const models = [{ id: '', label: 'Default' }];
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
models.push({ id: line, label: line });
|
|
281
|
+
}
|
|
282
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
283
|
+
return models;
|
|
284
|
+
}
|
|
285
|
+
} catch (_) {}
|
|
239
286
|
}
|
|
287
|
+
|
|
240
288
|
return [];
|
|
241
289
|
}
|
|
242
290
|
|
package/static/js/client.js
CHANGED
|
@@ -813,6 +813,7 @@ class AgentGUIClient {
|
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
if (data.message.role === 'user') {
|
|
816
|
+
// Find pending message by matching content to avoid duplicates
|
|
816
817
|
const pending = outputEl.querySelector('.message-sending');
|
|
817
818
|
if (pending) {
|
|
818
819
|
pending.id = '';
|
|
@@ -826,6 +827,12 @@ class AgentGUIClient {
|
|
|
826
827
|
this.emit('message:created', data);
|
|
827
828
|
return;
|
|
828
829
|
}
|
|
830
|
+
// Check if a user message with this ID already exists (prevents duplicate on race condition)
|
|
831
|
+
const existingMsg = outputEl.querySelector(`[data-msg-id="${data.message.id}"]`);
|
|
832
|
+
if (existingMsg) {
|
|
833
|
+
this.emit('message:created', data);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
829
836
|
}
|
|
830
837
|
|
|
831
838
|
const messageHtml = `
|
|
@@ -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;
|