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.
@@ -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
- proc.kill();
352
- resolve({ outputs, sessionId });
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
- proc.kill();
361
- reject(new Error(message.error.message || 'ACP prompt error'));
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 || completed) return;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.226",
3
+ "version": "1.0.227",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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
- if (agentId === 'claude-code') {
209
- return [
210
- { id: '', label: 'Default' },
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
- if (agentId === 'opencode') {
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('opencode models 2>/dev/null', { encoding: 'utf-8', timeout: 10000 });
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
- if (agentId === 'gemini') {
233
- return [
234
- { id: '', label: 'Default' },
235
- { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
236
- { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
237
- { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }
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
 
@@ -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 || 'unknown';
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 || 'unknown';
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];
@@ -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;