agentgui 1.0.241 → 1.0.243

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.
@@ -197,7 +197,7 @@ class AgentRunner {
197
197
  try {
198
198
  return await this._runACPOnce(prompt, cwd, config);
199
199
  } catch (err) {
200
- const isEmptyExit = err.message && err.message.includes('ACP exited with code');
200
+ const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
201
201
  const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
202
202
  if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
203
203
  const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
@@ -205,6 +205,13 @@ class AgentRunner {
205
205
  await new Promise(r => setTimeout(r, delay));
206
206
  return this.runACP(prompt, cwd, config, _retryCount + 1);
207
207
  }
208
+ if (err.isPrematureEnd) {
209
+ const premErr = new Error(err.message);
210
+ premErr.isPrematureEnd = true;
211
+ premErr.exitCode = err.exitCode;
212
+ premErr.stderrText = err.stderrText;
213
+ throw premErr;
214
+ }
208
215
  throw err;
209
216
  }
210
217
  }
@@ -437,7 +444,11 @@ class AgentRunner {
437
444
  resolve({ outputs, sessionId });
438
445
  } else {
439
446
  const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
440
- reject(new Error(`${this.name} ACP exited with code ${code}${detail}`));
447
+ const err = new Error(`${this.name} ACP exited with code ${code}${detail}`);
448
+ err.isPrematureEnd = true;
449
+ err.exitCode = code;
450
+ err.stderrText = stderrText;
451
+ reject(err);
441
452
  }
442
453
  });
443
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.241",
3
+ "version": "1.0.243",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -35,62 +35,71 @@ const modelDownloadState = {
35
35
  complete: false
36
36
  };
37
37
 
38
+ function broadcastModelProgress(progress) {
39
+ modelDownloadState.progress = progress;
40
+ broadcastSync({ type: 'model_download_progress', progress });
41
+ }
42
+
38
43
  async function ensureModelsDownloaded() {
39
- const { createRequire: cr } = await import('module');
40
- const r = cr(import.meta.url);
41
- const models = r('sttttsmodels');
42
- const checkAllFilesExist = models.checkAllFilesExist;
43
- const downloadModels = models.downloadModels;
44
-
45
- if (checkAllFilesExist && checkAllFilesExist()) {
46
- modelDownloadState.complete = true;
47
- return true;
48
- }
49
-
50
- if (!downloadModels) {
51
- console.log('[MODELS] Download function not available, skipping');
52
- modelDownloadState.complete = true;
53
- return true;
54
- }
55
-
56
44
  if (modelDownloadState.downloading) {
57
- // Wait for current download
58
45
  while (modelDownloadState.downloading) {
59
46
  await new Promise(r => setTimeout(r, 100));
60
47
  }
61
48
  return modelDownloadState.complete;
62
49
  }
63
-
64
- modelDownloadState.downloading = true;
65
- modelDownloadState.error = null;
66
-
50
+
67
51
  try {
68
- await downloadModels((progress) => {
69
- modelDownloadState.progress = progress;
70
- // Broadcast progress to all connected clients
71
- broadcastSync({
72
- type: 'model_download_progress',
73
- progress: {
74
- started: progress.started,
75
- done: progress.done,
76
- error: progress.error,
77
- downloading: progress.downloading,
78
- type: progress.type,
79
- completedFiles: progress.completedFiles,
80
- totalFiles: progress.totalFiles,
81
- totalDownloaded: progress.totalDownloaded,
82
- totalBytes: progress.totalBytes
83
- }
84
- });
85
- });
52
+ const { createRequire: cr } = await import('module');
53
+ const r = cr(import.meta.url);
54
+ const sttttsmodels = r('sttttsmodels');
55
+ const { sttDir, ttsDir } = sttttsmodels;
56
+
57
+ const sttOk = fs.existsSync(sttDir) && fs.readdirSync(sttDir).length > 0;
58
+ const ttsOk = fs.existsSync(ttsDir) && fs.readdirSync(ttsDir).length > 0;
59
+
60
+ if (sttOk && ttsOk) {
61
+ console.log('[MODELS] All model files present');
62
+ modelDownloadState.complete = true;
63
+ return true;
64
+ }
65
+
66
+ modelDownloadState.downloading = true;
67
+ modelDownloadState.error = null;
68
+
69
+ const webtalkWhisper = r('webtalk/whisper-models');
70
+ const webtalkTTS = r('webtalk/tts-models');
71
+ const { createConfig } = r('webtalk/config');
72
+ const config = createConfig({ sdkDir: path.dirname(fileURLToPath(import.meta.url)) });
73
+ config.modelsDir = path.dirname(sttDir);
74
+ config.ttsModelsDir = ttsDir;
75
+ config.sttModelsDir = sttDir;
76
+ config.whisperBaseUrl = 'https://huggingface.co/onnx-community/whisper-base/resolve/main/';
77
+ config.ttsBaseUrl = 'https://huggingface.co/datasets/AnEntrypoint/sttttsmodels/resolve/main/tts/';
78
+
79
+ const totalFiles = 16;
80
+ let completedFiles = 0;
81
+
82
+ if (!sttOk) {
83
+ console.log('[MODELS] Downloading STT model...');
84
+ broadcastModelProgress({ started: true, done: false, downloading: true, type: 'stt', completedFiles, totalFiles });
85
+ await webtalkWhisper.ensureModel('whisper-base', config);
86
+ completedFiles += 10;
87
+ }
88
+
89
+ if (!ttsOk) {
90
+ console.log('[MODELS] Downloading TTS models...');
91
+ broadcastModelProgress({ started: true, done: false, downloading: true, type: 'tts', completedFiles, totalFiles });
92
+ await webtalkTTS.ensureTTSModels(config);
93
+ completedFiles += 6;
94
+ }
95
+
86
96
  modelDownloadState.complete = true;
97
+ broadcastModelProgress({ started: true, done: true, downloading: false, completedFiles: totalFiles, totalFiles });
87
98
  return true;
88
99
  } catch (err) {
100
+ console.error('[MODELS] Download error:', err.message);
89
101
  modelDownloadState.error = err.message;
90
- broadcastSync({
91
- type: 'model_download_progress',
92
- progress: { error: err.message, done: true }
93
- });
102
+ broadcastModelProgress({ done: true, error: err.message });
94
103
  return false;
95
104
  } finally {
96
105
  modelDownloadState.downloading = false;
@@ -327,12 +336,57 @@ const AGENT_DEFAULT_MODELS = {
327
336
  ]
328
337
  };
329
338
 
339
+ async function fetchClaudeModelsFromAPI() {
340
+ const apiKey = process.env.ANTHROPIC_API_KEY;
341
+ if (!apiKey) return null;
342
+ try {
343
+ const https = await import('https');
344
+ return new Promise((resolve) => {
345
+ const req = https.default.request({
346
+ hostname: 'api.anthropic.com', path: '/v1/models', method: 'GET',
347
+ headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
348
+ timeout: 8000
349
+ }, (res) => {
350
+ let body = '';
351
+ res.on('data', d => body += d);
352
+ res.on('end', () => {
353
+ try {
354
+ const data = JSON.parse(body);
355
+ const items = (data.data || []).filter(m => m.id && m.id.startsWith('claude-'));
356
+ if (items.length === 0) return resolve(null);
357
+ const models = [{ id: '', label: 'Default' }];
358
+ for (const m of items) {
359
+ const label = m.display_name || m.id.replace(/^claude-/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
360
+ models.push({ id: m.id, label });
361
+ }
362
+ resolve(models);
363
+ } catch { resolve(null); }
364
+ });
365
+ });
366
+ req.on('error', () => resolve(null));
367
+ req.on('timeout', () => { req.destroy(); resolve(null); });
368
+ req.end();
369
+ });
370
+ } catch { return null; }
371
+ }
372
+
330
373
  async function getModelsForAgent(agentId) {
331
374
  const cached = modelCache.get(agentId);
332
- if (cached && Date.now() - cached.timestamp < 300000) {
375
+ if (cached && Date.now() - cached.timestamp < 3600000) {
333
376
  return cached.models;
334
377
  }
335
378
 
379
+ if (agentId === 'claude-code') {
380
+ const apiModels = await fetchClaudeModelsFromAPI();
381
+ if (apiModels) {
382
+ modelCache.set(agentId, { models: apiModels, timestamp: Date.now() });
383
+ return apiModels;
384
+ }
385
+ const models = AGENT_DEFAULT_MODELS[agentId];
386
+ modelCache.set(agentId, { models, timestamp: Date.now() });
387
+ return models;
388
+ }
389
+
336
390
  if (AGENT_DEFAULT_MODELS[agentId]) {
337
391
  const models = AGENT_DEFAULT_MODELS[agentId];
338
392
  modelCache.set(agentId, { models, timestamp: Date.now() });
@@ -2975,6 +3029,8 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
2975
3029
  sessionId,
2976
3030
  conversationId,
2977
3031
  error: error.message,
3032
+ isPrematureEnd: error.isPrematureEnd || false,
3033
+ exitCode: error.exitCode,
2978
3034
  recoverable: elapsed < 60000,
2979
3035
  timestamp: Date.now()
2980
3036
  });
package/static/index.html CHANGED
@@ -48,6 +48,8 @@
48
48
  --block-color-7: #f43f5e;
49
49
  --block-color-8: #22c55e;
50
50
  --block-color-9: #eab308;
51
+ --block-color-10: #0ea5e9;
52
+ --block-color-11: #d946ef;
51
53
  }
52
54
 
53
55
  html.dark {
@@ -66,6 +68,8 @@
66
68
  --block-color-7: #fb7185;
67
69
  --block-color-8: #4ade80;
68
70
  --block-color-9: #facc15;
71
+ --block-color-10: #38bdf8;
72
+ --block-color-11: #e879f9;
69
73
  }
70
74
 
71
75
  html, body {
@@ -1883,33 +1887,15 @@
1883
1887
  padding: 0.3rem 0.625rem;
1884
1888
  font-size: 0.75rem;
1885
1889
  line-height: 1.3;
1886
- cursor: pointer;
1890
+ cursor: default;
1887
1891
  user-select: none;
1888
- list-style: none;
1889
1892
  }
1890
1893
  .tool-result-status::-webkit-details-marker { display: none; }
1891
1894
  .tool-result-status::marker { display: none; content: ''; }
1892
- .tool-result-status::before {
1893
- content: '\25b6';
1894
- font-size: 0.5rem;
1895
- margin-right: 0.125rem;
1896
- display: inline-block;
1897
- transition: transform 0.15s;
1898
- color: #16a34a;
1899
- flex-shrink: 0;
1900
- }
1901
- html.dark .tool-result-status::before { color: #4ade80; }
1902
- .tool-result-inline[open] > .tool-result-status::before { transform: rotate(90deg); }
1903
- .tool-result-status:hover { background: #bbf7d0; }
1904
- html.dark .tool-result-status:hover { background: #14532d; }
1905
1895
  .tool-result-inline > .folded-tool-body { border-top: 1px solid #bbf7d0; }
1906
1896
  html.dark .tool-result-inline > .folded-tool-body { border-top-color: #166534; }
1907
1897
  .tool-result-error { background: #fef2f2; border-top-color: #fecaca; }
1908
1898
  html.dark .tool-result-error { background: #1f0a0a; border-top-color: #991b1b; }
1909
- .tool-result-error > .tool-result-status:hover { background: #fecaca; }
1910
- html.dark .tool-result-error > .tool-result-status:hover { background: #2d0f0f; }
1911
- .tool-result-error .tool-result-status::before { color: #dc2626; }
1912
- html.dark .tool-result-error .tool-result-status::before { color: #f87171; }
1913
1899
  .tool-result-error .folded-tool-icon { color: #dc2626; }
1914
1900
  html.dark .tool-result-error .folded-tool-icon { color: #f87171; }
1915
1901
  .tool-result-error .folded-tool-name { color: #991b1b; }
@@ -2390,6 +2376,174 @@
2390
2376
  animation: pulse 1s ease-in-out infinite;
2391
2377
  background-color: var(--color-warning) !important;
2392
2378
  }
2379
+
2380
+ /* ===== IN-UI DIALOGS ===== */
2381
+ .dialog-overlay {
2382
+ position: fixed;
2383
+ top: 0;
2384
+ left: 0;
2385
+ right: 0;
2386
+ bottom: 0;
2387
+ z-index: 10000;
2388
+ display: flex;
2389
+ align-items: center;
2390
+ justify-content: center;
2391
+ opacity: 0;
2392
+ transition: opacity 0.2s ease;
2393
+ }
2394
+ .dialog-overlay.visible { opacity: 1; }
2395
+ .dialog-backdrop {
2396
+ position: absolute;
2397
+ top: 0;
2398
+ left: 0;
2399
+ right: 0;
2400
+ bottom: 0;
2401
+ background: rgba(0, 0, 0, 0.5);
2402
+ backdrop-filter: blur(2px);
2403
+ }
2404
+ .dialog-container {
2405
+ position: fixed;
2406
+ top: 0;
2407
+ left: 0;
2408
+ right: 0;
2409
+ bottom: 0;
2410
+ display: flex;
2411
+ align-items: center;
2412
+ justify-content: center;
2413
+ z-index: 10001;
2414
+ opacity: 0;
2415
+ transition: opacity 0.2s ease;
2416
+ }
2417
+ .dialog-container.visible { opacity: 1; }
2418
+ .dialog-box {
2419
+ background: var(--color-bg-primary);
2420
+ border-radius: 0.75rem;
2421
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
2422
+ max-width: 90vw;
2423
+ width: 400px;
2424
+ overflow: hidden;
2425
+ transform: scale(0.95) translateY(-10px);
2426
+ transition: transform 0.2s ease;
2427
+ }
2428
+ .dialog-container.visible .dialog-box {
2429
+ transform: scale(1) translateY(0);
2430
+ }
2431
+ .dialog-box-progress { width: 450px; }
2432
+ .dialog-header {
2433
+ padding: 1rem 1.25rem;
2434
+ border-bottom: 1px solid var(--color-border);
2435
+ }
2436
+ .dialog-title {
2437
+ margin: 0;
2438
+ font-size: 1.1rem;
2439
+ font-weight: 600;
2440
+ }
2441
+ .dialog-body {
2442
+ padding: 1.25rem;
2443
+ }
2444
+ .dialog-message {
2445
+ margin: 0;
2446
+ font-size: 0.9rem;
2447
+ line-height: 1.5;
2448
+ color: var(--color-text-primary);
2449
+ }
2450
+ .dialog-label {
2451
+ display: block;
2452
+ margin-bottom: 0.5rem;
2453
+ font-size: 0.9rem;
2454
+ color: var(--color-text-primary);
2455
+ }
2456
+ .dialog-input {
2457
+ width: 100%;
2458
+ padding: 0.625rem 0.875rem;
2459
+ border: 1px solid var(--color-border);
2460
+ border-radius: 0.5rem;
2461
+ background: var(--color-bg-secondary);
2462
+ color: var(--color-text-primary);
2463
+ font-size: 0.9rem;
2464
+ outline: none;
2465
+ transition: border-color 0.15s, box-shadow 0.15s;
2466
+ }
2467
+ .dialog-input:focus {
2468
+ border-color: var(--color-primary);
2469
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
2470
+ }
2471
+ .dialog-footer {
2472
+ padding: 0.75rem 1.25rem;
2473
+ display: flex;
2474
+ justify-content: flex-end;
2475
+ gap: 0.5rem;
2476
+ border-top: 1px solid var(--color-border);
2477
+ background: var(--color-bg-secondary);
2478
+ }
2479
+ .dialog-btn {
2480
+ padding: 0.5rem 1.25rem;
2481
+ border: none;
2482
+ border-radius: 0.5rem;
2483
+ font-size: 0.875rem;
2484
+ font-weight: 500;
2485
+ cursor: pointer;
2486
+ transition: all 0.15s;
2487
+ }
2488
+ .dialog-btn-primary {
2489
+ background: var(--color-primary);
2490
+ color: white;
2491
+ }
2492
+ .dialog-btn-primary:hover { background: var(--color-primary-dark); }
2493
+ .dialog-btn-secondary {
2494
+ background: var(--color-bg-primary);
2495
+ color: var(--color-text-primary);
2496
+ border: 1px solid var(--color-border);
2497
+ }
2498
+ .dialog-btn-secondary:hover { background: var(--color-bg-secondary); }
2499
+ .dialog-btn-danger { background: var(--color-error); }
2500
+ .dialog-btn-danger:hover { background: #dc2626; }
2501
+ .dialog-progress-bar {
2502
+ height: 8px;
2503
+ background: var(--color-bg-secondary);
2504
+ border-radius: 4px;
2505
+ overflow: hidden;
2506
+ margin: 1rem 0 0.5rem;
2507
+ }
2508
+ .dialog-progress-fill {
2509
+ height: 100%;
2510
+ background: var(--color-primary);
2511
+ border-radius: 4px;
2512
+ transition: width 0.3s ease;
2513
+ }
2514
+ .dialog-progress-percent {
2515
+ margin: 0;
2516
+ text-align: center;
2517
+ font-size: 0.8rem;
2518
+ color: var(--color-text-secondary);
2519
+ }
2520
+
2521
+ /* ===== TOAST NOTIFICATIONS ===== */
2522
+ .toast-notification {
2523
+ position: fixed;
2524
+ bottom: 100px;
2525
+ left: 50%;
2526
+ transform: translateX(-50%) translateY(20px);
2527
+ padding: 0.75rem 1.5rem;
2528
+ border-radius: 0.5rem;
2529
+ font-size: 0.875rem;
2530
+ font-weight: 500;
2531
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
2532
+ z-index: 20000;
2533
+ opacity: 0;
2534
+ transition: all 0.3s ease;
2535
+ display: flex;
2536
+ align-items: center;
2537
+ gap: 0.5rem;
2538
+ }
2539
+ .toast-notification.visible {
2540
+ opacity: 1;
2541
+ transform: translateX(-50%) translateY(0);
2542
+ }
2543
+ .toast-info { background: var(--color-primary); color: white; }
2544
+ .toast-success { background: var(--color-success); color: white; }
2545
+ .toast-error { background: var(--color-error); color: white; }
2546
+ .toast-warning { background: var(--color-warning); color: white; }
2393
2547
  </style>
2394
2548
  </head>
2395
2549
  <body>
@@ -2597,6 +2751,7 @@
2597
2751
  <script defer src="/gm/js/websocket-manager.js"></script>
2598
2752
  <script defer src="/gm/js/event-filter.js"></script>
2599
2753
  <script defer src="/gm/js/syntax-highlighter.js"></script>
2754
+ <script defer src="/gm/js/dialogs.js"></script>
2600
2755
  <script defer src="/gm/js/ui-components.js"></script>
2601
2756
  <script defer src="/gm/js/conversations.js"></script>
2602
2757
  <script defer src="/gm/js/client.js"></script>
@@ -282,10 +282,12 @@ class AgentGUIClient {
282
282
 
283
283
  try {
284
284
  const position = localStorage.getItem(`scroll_${conversationId}`);
285
+ const scrollContainer = document.getElementById(this.config.scrollContainerId);
286
+ if (!scrollContainer) return;
287
+
285
288
  if (position !== null) {
286
289
  const scrollTop = parseInt(position, 10);
287
- const scrollContainer = document.getElementById(this.config.scrollContainerId);
288
- if (scrollContainer && !isNaN(scrollTop)) {
290
+ if (!isNaN(scrollTop)) {
289
291
  requestAnimationFrame(() => {
290
292
  requestAnimationFrame(() => {
291
293
  const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
@@ -293,6 +295,12 @@ class AgentGUIClient {
293
295
  });
294
296
  });
295
297
  }
298
+ } else {
299
+ requestAnimationFrame(() => {
300
+ requestAnimationFrame(() => {
301
+ scrollContainer.scrollTop = 0;
302
+ });
303
+ });
296
304
  }
297
305
  } catch (e) {
298
306
  console.warn('Failed to restore scroll position:', e);
@@ -368,7 +376,7 @@ class AgentGUIClient {
368
376
  if (this.ui.injectButton) {
369
377
  this.ui.injectButton.addEventListener('click', async () => {
370
378
  if (!this.state.currentConversation) return;
371
- const instructions = prompt('Enter instructions to inject into the running agent:');
379
+ const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
372
380
  if (!instructions) return;
373
381
  try {
374
382
  const resp = await fetch(`${window.__BASE_URL}/api/conversations/${this.state.currentConversation.id}/inject`, {
@@ -981,17 +989,17 @@ class AgentGUIClient {
981
989
  btn.addEventListener('click', async (e) => {
982
990
  const index = parseInt(e.target.dataset.index);
983
991
  const msgId = queue[index].messageId;
984
- if (confirm('Delete this queued message?')) {
992
+ if (await window.UIDialog.confirm('Delete this queued message?', 'Delete Message')) {
985
993
  await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${msgId}`, { method: 'DELETE' });
986
994
  }
987
995
  });
988
996
  });
989
997
 
990
998
  queueEl.querySelectorAll('.queue-edit-btn').forEach(btn => {
991
- btn.addEventListener('click', (e) => {
999
+ btn.addEventListener('click', async (e) => {
992
1000
  const index = parseInt(e.target.dataset.index);
993
1001
  const q = queue[index];
994
- const newContent = prompt('Edit message:', q.content);
1002
+ const newContent = await window.UIDialog.prompt('Edit message:', q.content, 'Edit Queued Message');
995
1003
  if (newContent !== null && newContent !== q.content) {
996
1004
  fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${q.messageId}`, {
997
1005
  method: 'PATCH',
@@ -1176,7 +1184,8 @@ class AgentGUIClient {
1176
1184
  const dName = hasRenderer ? StreamingRenderer.getToolDisplayName(tn) : tn;
1177
1185
  const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
1178
1186
  const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
1179
- html += `<details class="block-tool-use folded-tool"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
1187
+ const colorIdx = hasRenderer && this.renderer ? this.renderer._getBlockColorIndex('tool_use') : 1;
1188
+ html += `<details class="block-tool-use folded-tool" open style="border-left:3px solid var(--block-color-${colorIdx})"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
1180
1189
  pendingToolUseClose = true;
1181
1190
  } else if (block.type === 'tool_result') {
1182
1191
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
@@ -1185,7 +1194,8 @@ class AgentGUIClient {
1185
1194
  const resultIcon = block.is_error
1186
1195
  ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1187
1196
  : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1188
- const resultHtml = `<details class="tool-result-inline${block.is_error ? ' tool-result-error' : ''}"><summary class="tool-result-status"><span class="folded-tool-icon">${resultIcon}</span><span class="folded-tool-name">${block.is_error ? 'Error' : 'Success'}</span><span class="folded-tool-desc">${this.escapeHtml(resultPreview)}</span></summary><div class="folded-tool-body">${smartHtml}</div></details>`;
1197
+ const colorIdx = hasRenderer && this.renderer ? this.renderer._getBlockColorIndex('tool_result') : 2;
1198
+ const resultHtml = `<div class="tool-result-inline${block.is_error ? ' tool-result-error' : ''}" style="border-left:3px solid var(--block-color-${colorIdx})"><div class="tool-result-status"><span class="folded-tool-icon">${resultIcon}</span><span class="folded-tool-name">${block.is_error ? 'Error' : 'Success'}</span><span class="folded-tool-desc">${this.escapeHtml(resultPreview)}</span></div><div class="folded-tool-body">${smartHtml}</div></div>`;
1189
1199
  if (pendingToolUseClose) {
1190
1200
  html += resultHtml + '</details>';
1191
1201
  pendingToolUseClose = false;
@@ -1996,6 +2006,13 @@ class AgentGUIClient {
1996
2006
  this._modelDownloadInProgress = false;
1997
2007
  console.error('[Models] Download error:', progress.error);
1998
2008
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2009
+ if (window._voiceProgressDialog) {
2010
+ window._voiceProgressDialog.close();
2011
+ window._voiceProgressDialog = null;
2012
+ }
2013
+ if (window.UIDialog) {
2014
+ window.UIDialog.alert('Failed to download voice models: ' + progress.error, 'Download Error');
2015
+ }
1999
2016
  return;
2000
2017
  }
2001
2018
 
@@ -2004,12 +2021,30 @@ class AgentGUIClient {
2004
2021
  console.log('[Models] Download complete');
2005
2022
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2006
2023
  this._updateVoiceTabState();
2024
+ if (window._voiceProgressDialog) {
2025
+ window._voiceProgressDialog.update(100, 'Voice models ready!');
2026
+ setTimeout(function() {
2027
+ if (window._voiceProgressDialog) {
2028
+ window._voiceProgressDialog.close();
2029
+ window._voiceProgressDialog = null;
2030
+ }
2031
+ }, 500);
2032
+ }
2007
2033
  return;
2008
2034
  }
2009
2035
 
2010
2036
  if (progress.started || progress.downloading) {
2011
2037
  this._modelDownloadInProgress = true;
2012
2038
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2039
+
2040
+ if (window._voiceProgressDialog && progress.totalBytes > 0) {
2041
+ var pct = Math.round((progress.totalDownloaded / progress.totalBytes) * 100);
2042
+ var mb = Math.round(progress.totalBytes / 1024 / 1024);
2043
+ var downloaded = Math.round((progress.totalDownloaded || 0) / 1024 / 1024);
2044
+ window._voiceProgressDialog.update(pct, 'Downloading ' + downloaded + 'MB / ' + mb + 'MB');
2045
+ } else if (window._voiceProgressDialog && progress.file) {
2046
+ window._voiceProgressDialog.update(0, 'Loading ' + progress.file + '...');
2047
+ }
2013
2048
  }
2014
2049
  }
2015
2050
 
@@ -2018,8 +2053,7 @@ class AgentGUIClient {
2018
2053
  if (voiceBtn) {
2019
2054
  var isReady = this._modelDownloadProgress?.done === true ||
2020
2055
  this._modelDownloadProgress?.complete === true;
2021
- voiceBtn.disabled = !isReady;
2022
- voiceBtn.title = isReady ? 'Voice' : 'Downloading voice models...';
2056
+ voiceBtn.title = isReady ? 'Voice' : 'Voice (click to download models)';
2023
2057
  }
2024
2058
  }
2025
2059
 
@@ -2438,8 +2472,9 @@ class AgentGUIClient {
2438
2472
  */
2439
2473
  showError(message) {
2440
2474
  console.error(message);
2441
- // Could display in a toast or alert
2442
- alert(message);
2475
+ if (window.UIDialog) {
2476
+ window.UIDialog.alert(message, 'Error');
2477
+ }
2443
2478
  }
2444
2479
 
2445
2480
  /**
@@ -300,6 +300,10 @@ class StreamingRenderer {
300
300
  return this.renderBlock(event.block, event);
301
301
  }
302
302
 
303
+ if (event.type === 'streaming_error' && event.isPrematureEnd) {
304
+ return this.renderBlockPremature({ type: 'premature', error: event.error, exitCode: event.exitCode });
305
+ }
306
+
303
307
  switch (event.type) {
304
308
  case 'streaming_start':
305
309
  return this.renderStreamingStart(event);
@@ -366,6 +370,8 @@ class StreamingRenderer {
366
370
  return this.renderBlockUsage(block, context);
367
371
  case 'plan':
368
372
  return this.renderBlockPlan(block, context);
373
+ case 'premature':
374
+ return this.renderBlockPremature(block, context);
369
375
  default:
370
376
  return this.renderBlockGeneric(block, context);
371
377
  }
@@ -399,23 +405,28 @@ class StreamingRenderer {
399
405
  div.className = 'block-text';
400
406
  if (isHtml) div.classList.add('html-content');
401
407
  div.innerHTML = html;
402
- const colorIndex = this._getUniqueColorIndex(text);
408
+ const colorIndex = this._getBlockColorIndex('text');
403
409
  div.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
404
410
  return div;
405
411
  }
406
412
 
407
- _getUniqueColorIndex(text) {
408
- if (!this._colorIndexMap) {
409
- this._colorIndexMap = new Map();
410
- this._colorCounter = 0;
411
- }
412
- let index = this._colorIndexMap.get(text);
413
- if (index === undefined) {
414
- index = this._colorCounter % 10;
415
- this._colorIndexMap.set(text, index);
416
- this._colorCounter++;
417
- }
418
- return index;
413
+ _getBlockColorIndex(blockType) {
414
+ const typeColors = {
415
+ 'text': 0,
416
+ 'tool_use': 1,
417
+ 'tool_result': 2,
418
+ 'code': 3,
419
+ 'thinking': 4,
420
+ 'bash': 5,
421
+ 'system': 6,
422
+ 'result': 7,
423
+ 'error': 8,
424
+ 'image': 9,
425
+ 'plan': 10,
426
+ 'usage': 11,
427
+ 'premature': 8
428
+ };
429
+ return typeColors[blockType] !== undefined ? typeColors[blockType] : 0;
419
430
  }
420
431
 
421
432
  containsHtmlTags(text) {
@@ -463,6 +474,7 @@ class StreamingRenderer {
463
474
  renderBlockCode(block, context) {
464
475
  const div = document.createElement('div');
465
476
  div.className = 'block-code';
477
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('code')})`;
466
478
 
467
479
  const code = block.code || '';
468
480
  const language = (block.language || 'plaintext').toLowerCase();
@@ -508,6 +520,8 @@ class StreamingRenderer {
508
520
  renderBlockThinking(block, context) {
509
521
  const div = document.createElement('div');
510
522
  div.className = 'block-thinking';
523
+ const colorIndex = this._getBlockColorIndex('thinking');
524
+ div.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
511
525
 
512
526
  const thinking = block.thinking || '';
513
527
  div.innerHTML = `
@@ -733,7 +747,10 @@ class StreamingRenderer {
733
747
 
734
748
  const details = document.createElement('details');
735
749
  details.className = 'block-tool-use folded-tool';
750
+ details.setAttribute('open', '');
736
751
  if (block.id) details.dataset.toolUseId = block.id;
752
+ const colorIndex = this._getBlockColorIndex('tool_use');
753
+ details.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
737
754
  const summary = document.createElement('summary');
738
755
  summary.className = 'folded-tool-bar';
739
756
  const displayName = this.getToolUseDisplayName(toolName);
@@ -1199,33 +1216,34 @@ class StreamingRenderer {
1199
1216
  const isError = block.is_error || false;
1200
1217
  const content = block.content || '';
1201
1218
  const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
1202
- const preview = contentStr.length > 80 ? contentStr.substring(0, 77).replace(/\n/g, ' ') + '...' : contentStr.replace(/\n/g, ' ');
1203
1219
 
1204
- const details = document.createElement('details');
1205
- details.className = 'tool-result-inline' + (isError ? ' tool-result-error' : '');
1206
- details.dataset.eventType = 'tool_result';
1207
- if (block.tool_use_id) details.dataset.toolUseId = block.tool_use_id;
1220
+ const wrapper = document.createElement('div');
1221
+ wrapper.className = 'tool-result-inline' + (isError ? ' tool-result-error' : '');
1222
+ wrapper.dataset.eventType = 'tool_result';
1223
+ if (block.tool_use_id) wrapper.dataset.toolUseId = block.tool_use_id;
1224
+ const colorIndex = this._getBlockColorIndex('tool_result');
1225
+ wrapper.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
1208
1226
 
1227
+ const header = document.createElement('div');
1228
+ header.className = 'tool-result-status';
1209
1229
  const iconSvg = isError
1210
1230
  ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1211
1231
  : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1212
-
1213
- const summary = document.createElement('summary');
1214
- summary.className = 'tool-result-status';
1215
- summary.innerHTML = `
1232
+ const preview = contentStr.length > 80 ? contentStr.substring(0, 77).replace(/\n/g, ' ') + '...' : contentStr.replace(/\n/g, ' ');
1233
+ header.innerHTML = `
1216
1234
  <span class="folded-tool-icon">${iconSvg}</span>
1217
1235
  <span class="folded-tool-name">${isError ? 'Error' : 'Success'}</span>
1218
1236
  <span class="folded-tool-desc">${this.escapeHtml(preview)}</span>
1219
1237
  `;
1220
- details.appendChild(summary);
1238
+ wrapper.appendChild(header);
1221
1239
 
1222
1240
  const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this));
1223
1241
  const body = document.createElement('div');
1224
1242
  body.className = 'folded-tool-body';
1225
1243
  body.innerHTML = renderedContent;
1226
- details.appendChild(body);
1244
+ wrapper.appendChild(body);
1227
1245
 
1228
- return details;
1246
+ return wrapper;
1229
1247
  }
1230
1248
 
1231
1249
  /**
@@ -1234,6 +1252,7 @@ class StreamingRenderer {
1234
1252
  renderBlockImage(block, context) {
1235
1253
  const div = document.createElement('div');
1236
1254
  div.className = 'block-image';
1255
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('image')})`;
1237
1256
 
1238
1257
  let src = block.image || block.src || '';
1239
1258
  const alt = block.alt || 'Image';
@@ -1257,6 +1276,8 @@ class StreamingRenderer {
1257
1276
  renderBlockBash(block, context) {
1258
1277
  const div = document.createElement('div');
1259
1278
  div.className = 'block-bash';
1279
+ const colorIndex = this._getBlockColorIndex('bash');
1280
+ div.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
1260
1281
 
1261
1282
  const command = block.command || block.code || '';
1262
1283
  const output = block.output || '';
@@ -1284,6 +1305,7 @@ class StreamingRenderer {
1284
1305
  const details = document.createElement('details');
1285
1306
  details.className = 'folded-tool folded-tool-info';
1286
1307
  details.dataset.eventType = 'system';
1308
+ details.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('system')})`;
1287
1309
  const desc = block.model ? this.escapeHtml(block.model) : 'Session';
1288
1310
  const summary = document.createElement('summary');
1289
1311
  summary.className = 'folded-tool-bar';
@@ -1320,6 +1342,9 @@ class StreamingRenderer {
1320
1342
  const details = document.createElement('details');
1321
1343
  details.className = isError ? 'folded-tool folded-tool-error' : 'folded-tool';
1322
1344
  details.dataset.eventType = 'result';
1345
+ if (!isError) details.setAttribute('open', '');
1346
+ const colorIndex = this._getBlockColorIndex(isError ? 'error' : 'result');
1347
+ details.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
1323
1348
 
1324
1349
  const iconSvg = isError
1325
1350
  ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
@@ -1374,6 +1399,7 @@ class StreamingRenderer {
1374
1399
  const div = document.createElement('div');
1375
1400
  div.className = 'block-tool-status';
1376
1401
  div.dataset.toolUseId = block.tool_use_id || '';
1402
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('tool_use')})`;
1377
1403
  div.innerHTML = `
1378
1404
  <div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
1379
1405
  ${statusIcons[status] || statusIcons.pending}
@@ -1394,6 +1420,7 @@ class StreamingRenderer {
1394
1420
 
1395
1421
  const div = document.createElement('div');
1396
1422
  div.className = 'block-usage';
1423
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('usage')})`;
1397
1424
  div.innerHTML = `
1398
1425
  <div style="display:flex;gap:1rem;padding:0.25rem 0.5rem;font-size:0.7rem;color:var(--color-text-secondary);background:var(--color-bg-secondary);border-radius:0.25rem">
1399
1426
  ${used ? `<span><strong>Used:</strong> ${used.toLocaleString()}</span>` : ''}
@@ -1424,6 +1451,7 @@ class StreamingRenderer {
1424
1451
 
1425
1452
  const div = document.createElement('div');
1426
1453
  div.className = 'block-plan';
1454
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('plan')})`;
1427
1455
  div.innerHTML = `
1428
1456
  <details class="folded-tool folded-tool-info">
1429
1457
  <summary class="folded-tool-bar">
@@ -1446,6 +1474,21 @@ class StreamingRenderer {
1446
1474
  return div;
1447
1475
  }
1448
1476
 
1477
+ renderBlockPremature(block, context) {
1478
+ const div = document.createElement('div');
1479
+ div.className = 'folded-tool folded-tool-error block-premature';
1480
+ div.style.borderLeft = `3px solid var(--block-color-${this._getBlockColorIndex('premature')})`;
1481
+ const code = block.exitCode != null ? ` (exit ${block.exitCode})` : '';
1482
+ div.innerHTML = `
1483
+ <div class="folded-tool-bar" style="background:rgba(245,158,11,0.1)">
1484
+ <span class="folded-tool-icon" style="color:#f59e0b"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></span>
1485
+ <span class="folded-tool-name" style="color:#f59e0b">ACP Ended Prematurely${this.escapeHtml(code)}</span>
1486
+ <span class="folded-tool-desc">${this.escapeHtml(block.error || 'Process exited without output')}</span>
1487
+ </div>
1488
+ `;
1489
+ return div;
1490
+ }
1491
+
1449
1492
  /**
1450
1493
  * Render generic block with formatted key-value pairs
1451
1494
  */