agentgui 1.0.145 → 1.0.147

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/speech.js CHANGED
@@ -167,7 +167,11 @@ async function transcribe(audioBuffer) {
167
167
  const decoded = decodeWavToFloat32(buf);
168
168
  audio = resampleTo16k(decoded.audio, decoded.sampleRate);
169
169
  } else {
170
- audio = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
170
+ const sampleCount = Math.floor(buf.byteLength / 4);
171
+ if (sampleCount === 0) throw new Error('Audio buffer too small');
172
+ const aligned = new ArrayBuffer(sampleCount * 4);
173
+ new Uint8Array(aligned).set(buf.subarray(0, sampleCount * 4));
174
+ audio = new Float32Array(aligned);
171
175
  }
172
176
  const result = await stt(audio);
173
177
  return result.text || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.145",
3
+ "version": "1.0.147",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -15,7 +15,7 @@ const express = require('express');
15
15
  const Busboy = require('busboy');
16
16
  const fsbrowse = require('fsbrowse');
17
17
 
18
- const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
18
+ const SYSTEM_PROMPT = `Write all responses as clean semantic HTML. Use tags like <h3>, <p>, <ul>, <li>, <ol>, <table>, <code>, <pre>, <strong>, <em>, <a>, <blockquote>, <details>, <summary>. Your HTML will be rendered directly in a styled container that already provides fonts, colors, spacing, and dark mode support. Do not include <html>, <head>, <body>, <style>, or <script> tags. Do not use inline styles unless necessary for layout like tables. Do not use CSS class names. Just write semantic HTML content.`;
19
19
 
20
20
  const activeExecutions = new Map();
21
21
  const messageQueues = new Map();
@@ -522,7 +522,7 @@ const server = http.createServer(async (req, res) => {
522
522
  const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
523
523
  const contentType = mimeTypes[ext] || 'application/octet-stream';
524
524
  const fileContent = fs.readFileSync(normalizedPath);
525
- res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=3600' });
525
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
526
526
  res.end(fileContent);
527
527
  } catch (err) {
528
528
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -574,7 +574,7 @@ function serveFile(filePath, res) {
574
574
  res.writeHead(200, {
575
575
  'Content-Type': contentType,
576
576
  'Content-Length': stats.size,
577
- 'Cache-Control': 'public, max-age=3600'
577
+ 'Cache-Control': 'no-cache, must-revalidate'
578
578
  });
579
579
  fs.createReadStream(filePath).pipe(res);
580
580
  });
package/static/index.html CHANGED
@@ -524,12 +524,17 @@
524
524
  }
525
525
 
526
526
  .streaming-block-tool-use {
527
- margin: 0.5rem 0;
527
+ margin: 0.25rem 0;
528
528
  border-left: 3px solid #06b6d4;
529
529
  background: rgba(6,182,212,0.06);
530
530
  border-radius: 0 0.375rem 0.375rem 0;
531
531
  overflow: hidden;
532
532
  }
533
+ .streaming-block-tool-use.folded-tool {
534
+ border-left: none;
535
+ border-radius: 0.375rem;
536
+ margin: 0.125rem 0;
537
+ }
533
538
 
534
539
  .tool-use-header {
535
540
  padding: 0.5rem 0.75rem;
@@ -582,7 +587,7 @@
582
587
  html.dark .tool-input-pre { background: rgba(255,255,255,0.03); }
583
588
 
584
589
  .streaming-block-tool-result {
585
- margin: 0.25rem 0 0.5rem 0;
590
+ margin: 0.125rem 0 0.25rem 0;
586
591
  border-radius: 0.375rem;
587
592
  background: var(--color-bg-code);
588
593
  overflow: hidden;
@@ -1101,6 +1106,26 @@
1101
1106
  border-top: 1px solid var(--color-border);
1102
1107
  }
1103
1108
 
1109
+ .voice-reread-btn {
1110
+ position: absolute;
1111
+ top: 0.5rem;
1112
+ right: 0.5rem;
1113
+ background: none;
1114
+ border: 1px solid var(--color-border);
1115
+ border-radius: 0.25rem;
1116
+ cursor: pointer;
1117
+ padding: 0.25rem;
1118
+ color: var(--color-text-secondary);
1119
+ opacity: 0;
1120
+ transition: opacity 0.15s, background-color 0.15s;
1121
+ display: flex;
1122
+ align-items: center;
1123
+ justify-content: center;
1124
+ }
1125
+
1126
+ .voice-block:hover .voice-reread-btn { opacity: 1; }
1127
+ .voice-reread-btn:hover { background: var(--color-bg-primary); color: var(--color-primary); }
1128
+
1104
1129
  /* ===== RESPONSIVE: TABLET ===== */
1105
1130
  @media (min-width: 769px) and (max-width: 1024px) {
1106
1131
  :root { --sidebar-width: 260px; }
@@ -1218,7 +1243,7 @@
1218
1243
 
1219
1244
  /* --- Tool Use Block --- */
1220
1245
  .block-tool-use {
1221
- margin-bottom: 0.75rem;
1246
+ margin-bottom: 0.25rem;
1222
1247
  border-radius: 0.5rem;
1223
1248
  background: #ecfeff;
1224
1249
  overflow: hidden;
@@ -1227,10 +1252,10 @@
1227
1252
  html.dark .block-tool-use { background: #0c1a24; }
1228
1253
 
1229
1254
  .block-tool-use .tool-header {
1230
- padding: 0.625rem 1rem;
1255
+ padding: 0.375rem 0.75rem;
1231
1256
  display: flex;
1232
1257
  align-items: center;
1233
- gap: 0.5rem;
1258
+ gap: 0.375rem;
1234
1259
  background: #cffafe;
1235
1260
  }
1236
1261
 
@@ -1263,7 +1288,7 @@
1263
1288
  html.dark .block-tool-use .tool-header .tool-name code { background: #164e63; }
1264
1289
 
1265
1290
  .block-tool-use .tool-params {
1266
- padding: 0.75rem 1rem;
1291
+ padding: 0.5rem 0.75rem;
1267
1292
  }
1268
1293
 
1269
1294
  .block-tool-use .tool-params .param-label {
@@ -1458,9 +1483,94 @@
1458
1483
  text-align: center;
1459
1484
  }
1460
1485
 
1486
+ /* --- Folded Tool Use (compact success-style bar) --- */
1487
+ .folded-tool {
1488
+ margin: 0.25rem 0;
1489
+ border-radius: 0.375rem;
1490
+ overflow: hidden;
1491
+ background: #f0fdf4;
1492
+ border: 1px solid #bbf7d0;
1493
+ }
1494
+ html.dark .folded-tool {
1495
+ background: #0a1f0f;
1496
+ border-color: #166534;
1497
+ }
1498
+ .folded-tool-bar {
1499
+ display: flex;
1500
+ align-items: center;
1501
+ gap: 0.375rem;
1502
+ padding: 0.3rem 0.625rem;
1503
+ cursor: pointer;
1504
+ user-select: none;
1505
+ list-style: none;
1506
+ font-size: 0.75rem;
1507
+ line-height: 1.3;
1508
+ background: #dcfce7;
1509
+ transition: background 0.15s;
1510
+ }
1511
+ html.dark .folded-tool-bar {
1512
+ background: #0f2b1a;
1513
+ }
1514
+ .folded-tool-bar::-webkit-details-marker { display: none; }
1515
+ .folded-tool-bar::marker { display: none; content: ''; }
1516
+ .folded-tool-bar::before {
1517
+ content: '\25b6';
1518
+ font-size: 0.5rem;
1519
+ margin-right: 0.125rem;
1520
+ display: inline-block;
1521
+ transition: transform 0.15s;
1522
+ color: #16a34a;
1523
+ flex-shrink: 0;
1524
+ }
1525
+ html.dark .folded-tool-bar::before { color: #4ade80; }
1526
+ .folded-tool[open] > .folded-tool-bar::before { transform: rotate(90deg); }
1527
+ .folded-tool-bar:hover { background: #bbf7d0; }
1528
+ html.dark .folded-tool-bar:hover { background: #14532d; }
1529
+ .folded-tool-icon {
1530
+ display: flex;
1531
+ align-items: center;
1532
+ color: #16a34a;
1533
+ width: 0.875rem;
1534
+ height: 0.875rem;
1535
+ flex-shrink: 0;
1536
+ }
1537
+ html.dark .folded-tool-icon { color: #4ade80; }
1538
+ .folded-tool-icon svg { width: 0.875rem; height: 0.875rem; }
1539
+ .folded-tool-name {
1540
+ font-weight: 600;
1541
+ color: #166534;
1542
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
1543
+ font-size: 0.7rem;
1544
+ flex-shrink: 0;
1545
+ }
1546
+ html.dark .folded-tool-name { color: #86efac; }
1547
+ .folded-tool-desc {
1548
+ color: #15803d;
1549
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
1550
+ font-size: 0.7rem;
1551
+ overflow: hidden;
1552
+ text-overflow: ellipsis;
1553
+ white-space: nowrap;
1554
+ flex: 1;
1555
+ min-width: 0;
1556
+ opacity: 0.8;
1557
+ }
1558
+ html.dark .folded-tool-desc { color: #4ade80; }
1559
+ .folded-tool-body {
1560
+ padding: 0.5rem 0.75rem;
1561
+ font-size: 0.75rem;
1562
+ border-top: 1px solid #bbf7d0;
1563
+ }
1564
+ html.dark .folded-tool-body { border-top-color: #166534; }
1565
+ .folded-tool-body .tool-input-pre {
1566
+ margin: 0;
1567
+ padding: 0.375rem 0.5rem;
1568
+ font-size: 0.7rem;
1569
+ }
1570
+
1461
1571
  /* --- Tool Result Block --- */
1462
1572
  .block-tool-result {
1463
- margin-bottom: 0.75rem;
1573
+ margin-bottom: 0.25rem;
1464
1574
  border-radius: 0.5rem;
1465
1575
  overflow: hidden;
1466
1576
  }
@@ -1471,7 +1581,7 @@
1471
1581
  html.dark .block-tool-result.result-error { background: #1c0f0f; }
1472
1582
 
1473
1583
  .block-tool-result .result-header {
1474
- padding: 0.5rem 1rem;
1584
+ padding: 0.3rem 0.75rem;
1475
1585
  display: flex;
1476
1586
  align-items: center;
1477
1587
  justify-content: space-between;
@@ -516,7 +516,7 @@ class AgentGUIClient {
516
516
  if (block.type === 'text' && block.text) {
517
517
  const text = block.text;
518
518
  if (this.isHtmlContent(text)) {
519
- return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${text}</div>`;
519
+ return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(text)}</div>`;
520
520
  }
521
521
  const parts = this.parseMarkdownCodeBlocks(text);
522
522
  if (parts.length === 1 && parts[0].type === 'text') {
@@ -524,7 +524,7 @@ class AgentGUIClient {
524
524
  }
525
525
  return parts.map(part => {
526
526
  if (part.type === 'html') {
527
- return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${part.content}</div>`;
527
+ return `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(part.content)}</div>`;
528
528
  } else if (part.type === 'code') {
529
529
  return this.renderCodeBlock(part.language, part.code);
530
530
  }
@@ -682,9 +682,17 @@ class AgentGUIClient {
682
682
  }
683
683
 
684
684
  isHtmlContent(text) {
685
- const openTag = /<(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])\b[^>]*>/i;
686
- const closeTag = /<\/(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])>/i;
687
- return openTag.test(text) && closeTag.test(text);
685
+ const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
686
+ return htmlPattern.test(text);
687
+ }
688
+
689
+ sanitizeHtml(html) {
690
+ const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
691
+ let cleaned = html.replace(dangerous, '');
692
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
693
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
694
+ cleaned = cleaned.replace(/javascript\s*:/gi, '');
695
+ return cleaned;
688
696
  }
689
697
 
690
698
  parseMarkdownCodeBlocks(text) {
@@ -735,7 +743,7 @@ class AgentGUIClient {
735
743
  Rendered HTML
736
744
  </div>
737
745
  <div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
738
- ${code}
746
+ ${this.sanitizeHtml(code)}
739
747
  </div>
740
748
  </div>
741
749
  `;
@@ -751,7 +759,7 @@ class AgentGUIClient {
751
759
  renderMessageContent(content) {
752
760
  if (typeof content === 'string') {
753
761
  if (this.isHtmlContent(content)) {
754
- return `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${content}</div></div>`;
762
+ return `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(content)}</div></div>`;
755
763
  }
756
764
  return `<div class="message-text">${this.escapeHtml(content)}</div>`;
757
765
  } else if (content && typeof content === 'object' && content.type === 'claude_execution') {
@@ -762,7 +770,7 @@ class AgentGUIClient {
762
770
  const parts = this.parseMarkdownCodeBlocks(block.text);
763
771
  parts.forEach(part => {
764
772
  if (part.type === 'html') {
765
- html += `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${part.content}</div></div>`;
773
+ html += `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(part.content)}</div></div>`;
766
774
  } else if (part.type === 'text') {
767
775
  html += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
768
776
  } else if (part.type === 'code') {
@@ -778,7 +786,7 @@ class AgentGUIClient {
778
786
  Rendered HTML
779
787
  </div>
780
788
  <div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
781
- ${block.code}
789
+ ${this.sanitizeHtml(block.code)}
782
790
  </div>
783
791
  </div>
784
792
  `;
@@ -790,12 +798,14 @@ class AgentGUIClient {
790
798
  let inputHtml = '';
791
799
  if (block.input && Object.keys(block.input).length > 0) {
792
800
  const inputStr = JSON.stringify(block.input, null, 2);
793
- inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
801
+ inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
794
802
  }
795
803
  const tn = block.name || 'unknown';
796
804
  const foldable = tn.startsWith('mcp__') || tn === 'Edit';
797
805
  if (foldable) {
798
- html += `<details class="streaming-block-tool-use"><summary class="tool-use-header" style="cursor:pointer;user-select:none;list-style:none;"><span class="tool-use-icon">&#9881;</span> <span class="tool-use-name">${this.escapeHtml(tn)}</span></summary>${inputHtml}</details>`;
806
+ const dName = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.getToolDisplayName(tn) : tn;
807
+ const tTitle = typeof StreamingRenderer !== 'undefined' && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
808
+ html += `<details class="streaming-block-tool-use folded-tool"><summary class="folded-tool-bar"><span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}</details>`;
799
809
  } else {
800
810
  html += `<div class="streaming-block-tool-use"><div class="tool-use-header"><span class="tool-use-icon">&#9881;</span> <span class="tool-use-name">${this.escapeHtml(tn)}</span></div>${inputHtml}</div>`;
801
811
  }
@@ -1397,7 +1407,7 @@ class AgentGUIClient {
1397
1407
 
1398
1408
  if (typeof msg.content === 'string') {
1399
1409
  if (this.isHtmlContent(msg.content)) {
1400
- contentHtml = `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${msg.content}</div></div>`;
1410
+ contentHtml = `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(msg.content)}</div></div>`;
1401
1411
  } else {
1402
1412
  contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
1403
1413
  }
@@ -1437,12 +1447,14 @@ class AgentGUIClient {
1437
1447
  let inputHtml = '';
1438
1448
  if (block.input && Object.keys(block.input).length > 0) {
1439
1449
  const inputStr = JSON.stringify(block.input, null, 2);
1440
- inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
1450
+ inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
1441
1451
  }
1442
1452
  const tn2 = block.name || 'unknown';
1443
1453
  const foldable2 = tn2.startsWith('mcp__') || tn2 === 'Edit';
1444
1454
  if (foldable2) {
1445
- contentHtml += `<details class="streaming-block-tool-use"><summary class="tool-use-header" style="cursor:pointer;user-select:none;list-style:none;"><span class="tool-use-icon">&#9881;</span> <span class="tool-use-name">${this.escapeHtml(tn2)}</span></summary>${inputHtml}</details>`;
1455
+ const dName2 = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.getToolDisplayName(tn2) : tn2;
1456
+ const tTitle2 = typeof StreamingRenderer !== 'undefined' && block.input ? StreamingRenderer.getToolTitle(tn2, block.input) : '';
1457
+ contentHtml += `<details class="streaming-block-tool-use folded-tool"><summary class="folded-tool-bar"><span class="folded-tool-name">${this.escapeHtml(dName2)}</span>${tTitle2 ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle2)}</span>` : ''}</summary>${inputHtml}</details>`;
1446
1458
  } else {
1447
1459
  contentHtml += `<div class="streaming-block-tool-use"><div class="tool-use-header"><span class="tool-use-icon">&#9881;</span> <span class="tool-use-name">${this.escapeHtml(tn2)}</span></div>${inputHtml}</div>`;
1448
1460
  }
@@ -362,12 +362,30 @@ class StreamingRenderer {
362
362
  div.className = 'block-text';
363
363
 
364
364
  const text = block.text || '';
365
- const html = this.parseAndRenderMarkdown(text);
366
- div.innerHTML = html;
365
+ if (this.containsHtmlTags(text)) {
366
+ div.innerHTML = this.sanitizeHtml(text);
367
+ div.classList.add('html-content');
368
+ } else {
369
+ div.innerHTML = this.parseAndRenderMarkdown(text);
370
+ }
367
371
 
368
372
  return div;
369
373
  }
370
374
 
375
+ containsHtmlTags(text) {
376
+ const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
377
+ return htmlPattern.test(text);
378
+ }
379
+
380
+ sanitizeHtml(html) {
381
+ const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
382
+ let cleaned = html.replace(dangerous, '');
383
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
384
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
385
+ cleaned = cleaned.replace(/javascript\s*:/gi, '');
386
+ return cleaned;
387
+ }
388
+
371
389
  /**
372
390
  * Parse markdown and render links, code, bold, italic
373
391
  */
@@ -623,6 +641,53 @@ class StreamingRenderer {
623
641
  /**
624
642
  * Render tool use block with smart parameter display
625
643
  */
644
+ getToolUseTitle(toolName, input) {
645
+ const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
646
+ if (normalizedName === 'Edit' && input.file_path) {
647
+ const parts = input.file_path.split('/');
648
+ const fileName = parts.pop();
649
+ const dir = parts.slice(-2).join('/');
650
+ return dir ? `${dir}/${fileName}` : fileName;
651
+ }
652
+ if (normalizedName === 'Read' && input.file_path) {
653
+ const parts = input.file_path.split('/');
654
+ return parts.pop();
655
+ }
656
+ if (normalizedName === 'Write' && input.file_path) {
657
+ const parts = input.file_path.split('/');
658
+ return parts.pop();
659
+ }
660
+ if (normalizedName === 'Bash' || normalizedName === 'bash') {
661
+ const cmd = input.command || input.commands || '';
662
+ const cmdText = typeof cmd === 'string' ? cmd : JSON.stringify(cmd);
663
+ return cmdText.length > 60 ? cmdText.substring(0, 57) + '...' : cmdText;
664
+ }
665
+ if (normalizedName === 'Glob' && input.pattern) return input.pattern;
666
+ if (normalizedName === 'Grep' && input.pattern) return input.pattern;
667
+ if (normalizedName === 'WebFetch' && input.url) {
668
+ try { return new URL(input.url).hostname; } catch (e) { return input.url.substring(0, 40); }
669
+ }
670
+ if (normalizedName === 'WebSearch' && input.query) return input.query.substring(0, 50);
671
+ if (input.file_path) return input.file_path.split('/').pop();
672
+ if (input.command) {
673
+ const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command);
674
+ return c.length > 50 ? c.substring(0, 47) + '...' : c;
675
+ }
676
+ if (input.query) return input.query.substring(0, 50);
677
+ return '';
678
+ }
679
+
680
+ getToolUseDisplayName(toolName) {
681
+ const normalized = toolName.replace(/^mcp__[^_]+__/, '');
682
+ const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
683
+ if (knownTools.includes(normalized)) return normalized;
684
+ if (toolName.startsWith('mcp__')) {
685
+ const parts = toolName.split('__');
686
+ return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
687
+ }
688
+ return normalized || toolName;
689
+ }
690
+
626
691
  renderBlockToolUse(block, context) {
627
692
  const toolName = block.name || 'unknown';
628
693
  const input = block.input || {};
@@ -630,17 +695,20 @@ class StreamingRenderer {
630
695
 
631
696
  if (shouldFold) {
632
697
  const details = document.createElement('details');
633
- details.className = 'block-tool-use';
698
+ details.className = 'block-tool-use folded-tool';
634
699
  const summary = document.createElement('summary');
635
- summary.className = 'tool-header';
636
- summary.style.cssText = 'cursor:pointer;user-select:none;list-style:none;';
700
+ summary.className = 'folded-tool-bar';
701
+ const displayName = this.getToolUseDisplayName(toolName);
702
+ const titleInfo = this.getToolUseTitle(toolName, input);
637
703
  summary.innerHTML = `
638
- <span class="tool-icon">${this.getToolIcon(toolName)}</span>
639
- <span class="tool-name"><code>${this.escapeHtml(toolName)}</code></span>
704
+ <span class="folded-tool-icon">${this.getToolIcon(toolName)}</span>
705
+ <span class="folded-tool-name">${this.escapeHtml(displayName)}</span>
706
+ ${titleInfo ? `<span class="folded-tool-desc">${this.escapeHtml(titleInfo)}</span>` : ''}
640
707
  `;
641
708
  details.appendChild(summary);
642
709
  if (Object.keys(input).length > 0) {
643
710
  const paramsDiv = document.createElement('div');
711
+ paramsDiv.className = 'folded-tool-body';
644
712
  paramsDiv.innerHTML = this.renderSmartParams(toolName, input);
645
713
  details.appendChild(paramsDiv);
646
714
  }
@@ -1004,6 +1072,33 @@ class StreamingRenderer {
1004
1072
  return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
1005
1073
  }
1006
1074
 
1075
+ static getToolDisplayName(toolName) {
1076
+ const normalized = toolName.replace(/^mcp__[^_]+__/, '');
1077
+ const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
1078
+ if (knownTools.includes(normalized)) return normalized;
1079
+ if (toolName.startsWith('mcp__')) {
1080
+ const parts = toolName.split('__');
1081
+ return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
1082
+ }
1083
+ return normalized || toolName;
1084
+ }
1085
+
1086
+ static getToolTitle(toolName, input) {
1087
+ const n = toolName.replace(/^mcp__[^_]+__/, '');
1088
+ if (n === 'Edit' && input.file_path) { const p = input.file_path.split('/'); const f = p.pop(); const d = p.slice(-2).join('/'); return d ? d+'/'+f : f; }
1089
+ if (n === 'Read' && input.file_path) return input.file_path.split('/').pop();
1090
+ if (n === 'Write' && input.file_path) return input.file_path.split('/').pop();
1091
+ if ((n === 'Bash' || n === 'bash') && (input.command || input.commands)) { const c = typeof (input.command||input.commands) === 'string' ? (input.command||input.commands) : JSON.stringify(input.command||input.commands); return c.length > 60 ? c.substring(0,57)+'...' : c; }
1092
+ if (n === 'Glob' && input.pattern) return input.pattern;
1093
+ if (n === 'Grep' && input.pattern) return input.pattern;
1094
+ if (n === 'WebFetch' && input.url) { try { return new URL(input.url).hostname; } catch(e) { return input.url.substring(0,40); } }
1095
+ if (n === 'WebSearch' && input.query) return input.query.substring(0,50);
1096
+ if (input.file_path) return input.file_path.split('/').pop();
1097
+ if (input.command) { const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command); return c.length > 50 ? c.substring(0,47)+'...' : c; }
1098
+ if (input.query) return input.query.substring(0,50);
1099
+ return '';
1100
+ }
1101
+
1007
1102
  /**
1008
1103
  * Static HTML version of parameter rendering
1009
1104
  */
@@ -1182,7 +1277,7 @@ class StreamingRenderer {
1182
1277
  ${cost ? `<div class="result-stat"><span class="stat-icon">&#128176;</span><span class="stat-value">${this.escapeHtml(cost)}</span><span class="stat-label">cost</span></div>` : ''}
1183
1278
  ${turns ? `<div class="result-stat"><span class="stat-icon">&#128260;</span><span class="stat-value">${this.escapeHtml(String(turns))}</span><span class="stat-label">turns</span></div>` : ''}
1184
1279
  </div>
1185
- ${block.result ? `<div class="result-content">${this.escapeHtml(typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2))}</div>` : ''}
1280
+ ${block.result ? `<div class="result-content">${(() => { const r = typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2); return this.containsHtmlTags(r) ? '<div class="html-content">' + this.sanitizeHtml(r) + '</div>' : this.escapeHtml(r); })()}</div>` : ''}
1186
1281
  `;
1187
1282
 
1188
1283
  return div;
@@ -3,7 +3,6 @@
3
3
  var isRecording = false;
4
4
  var ttsEnabled = true;
5
5
  var voiceActive = false;
6
- var lastSpokenBlockIndex = -1;
7
6
  var currentConversationId = null;
8
7
  var speechQueue = [];
9
8
  var isSpeaking = false;
@@ -13,6 +12,8 @@
13
12
  var scriptNode = null;
14
13
  var recordedChunks = [];
15
14
  var TARGET_SAMPLE_RATE = 16000;
15
+ var spokenChunks = new Set();
16
+ var isLoadingHistory = false;
16
17
 
17
18
  function init() {
18
19
  setupTTSToggle();
@@ -61,14 +62,28 @@
61
62
  var micBtn = document.getElementById('voiceMicBtn');
62
63
  if (micBtn) {
63
64
  micBtn.removeAttribute('disabled');
64
- micBtn.title = 'Click to record';
65
- micBtn.addEventListener('click', function(e) {
65
+ micBtn.title = 'Hold to record';
66
+ micBtn.addEventListener('mousedown', function(e) {
66
67
  e.preventDefault();
67
- if (!isRecording) {
68
- startRecording();
69
- } else {
70
- stopRecording();
71
- }
68
+ startRecording();
69
+ });
70
+ micBtn.addEventListener('mouseup', function(e) {
71
+ e.preventDefault();
72
+ stopRecording();
73
+ });
74
+ micBtn.addEventListener('mouseleave', function(e) {
75
+ if (isRecording) stopRecording();
76
+ });
77
+ micBtn.addEventListener('touchstart', function(e) {
78
+ e.preventDefault();
79
+ startRecording();
80
+ });
81
+ micBtn.addEventListener('touchend', function(e) {
82
+ e.preventDefault();
83
+ stopRecording();
84
+ });
85
+ micBtn.addEventListener('touchcancel', function(e) {
86
+ if (isRecording) stopRecording();
72
87
  });
73
88
  }
74
89
  var sendBtn = document.getElementById('voiceSendBtn');
@@ -92,6 +107,35 @@
92
107
  return result;
93
108
  }
94
109
 
110
+ function encodeWav(float32Audio, sampleRate) {
111
+ var numSamples = float32Audio.length;
112
+ var bytesPerSample = 2;
113
+ var dataSize = numSamples * bytesPerSample;
114
+ var buffer = new ArrayBuffer(44 + dataSize);
115
+ var view = new DataView(buffer);
116
+ function writeStr(off, str) {
117
+ for (var i = 0; i < str.length; i++) view.setUint8(off + i, str.charCodeAt(i));
118
+ }
119
+ writeStr(0, 'RIFF');
120
+ view.setUint32(4, 36 + dataSize, true);
121
+ writeStr(8, 'WAVE');
122
+ writeStr(12, 'fmt ');
123
+ view.setUint32(16, 16, true);
124
+ view.setUint16(20, 1, true);
125
+ view.setUint16(22, 1, true);
126
+ view.setUint32(24, sampleRate, true);
127
+ view.setUint32(28, sampleRate * bytesPerSample, true);
128
+ view.setUint16(32, bytesPerSample, true);
129
+ view.setUint16(34, 16, true);
130
+ writeStr(36, 'data');
131
+ view.setUint32(40, dataSize, true);
132
+ for (var i = 0; i < numSamples; i++) {
133
+ var s = Math.max(-1, Math.min(1, float32Audio[i]));
134
+ view.setInt16(44 + i * 2, s < 0 ? s * 32768 : s * 32767, true);
135
+ }
136
+ return buffer;
137
+ }
138
+
95
139
  async function startRecording() {
96
140
  if (isRecording) return;
97
141
  var el = document.getElementById('voiceTranscript');
@@ -146,11 +190,11 @@
146
190
  var resampled = resampleBuffer(merged, sourceSampleRate, TARGET_SAMPLE_RATE);
147
191
  if (el) el.textContent = 'Transcribing...';
148
192
  try {
149
- var pcmBuffer = resampled.buffer;
193
+ var wavBuffer = encodeWav(resampled, TARGET_SAMPLE_RATE);
150
194
  var resp = await fetch(BASE + '/api/stt', {
151
195
  method: 'POST',
152
- headers: { 'Content-Type': 'application/octet-stream' },
153
- body: pcmBuffer
196
+ headers: { 'Content-Type': 'audio/wav' },
197
+ body: wavBuffer
154
198
  });
155
199
  var data = await resp.json();
156
200
  if (data.text) {
@@ -240,6 +284,10 @@
240
284
  }
241
285
  }
242
286
 
287
+ function stripHtml(text) {
288
+ return text.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
289
+ }
290
+
243
291
  function addVoiceBlock(text, isUser) {
244
292
  var container = document.getElementById('voiceMessages');
245
293
  if (!container) return;
@@ -247,13 +295,23 @@
247
295
  if (emptyMsg) emptyMsg.remove();
248
296
  var div = document.createElement('div');
249
297
  div.className = 'voice-block' + (isUser ? ' voice-block-user' : '');
250
- div.textContent = text;
298
+ div.textContent = isUser ? text : stripHtml(text);
299
+ if (!isUser) {
300
+ var rereadBtn = document.createElement('button');
301
+ rereadBtn.className = 'voice-reread-btn';
302
+ rereadBtn.title = 'Re-read aloud';
303
+ rereadBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>';
304
+ rereadBtn.addEventListener('click', function() {
305
+ speak(text);
306
+ });
307
+ div.appendChild(rereadBtn);
308
+ }
251
309
  container.appendChild(div);
252
310
  scrollVoiceToBottom();
253
311
  return div;
254
312
  }
255
313
 
256
- function addVoiceResultBlock(block) {
314
+ function addVoiceResultBlock(block, autoSpeak) {
257
315
  var container = document.getElementById('voiceMessages');
258
316
  if (!container) return;
259
317
  var emptyMsg = container.querySelector('.voice-empty');
@@ -267,9 +325,10 @@
267
325
  if (block.result) {
268
326
  resultText = typeof block.result === 'string' ? block.result : JSON.stringify(block.result);
269
327
  }
328
+ var displayText = stripHtml(resultText);
270
329
  var html = '';
271
- if (resultText) {
272
- html += '<div>' + escapeHtml(resultText) + '</div>';
330
+ if (displayText) {
331
+ html += '<div>' + escapeHtml(displayText) + '</div>';
273
332
  }
274
333
  if (duration || cost) {
275
334
  html += '<div class="voice-result-stats">';
@@ -282,9 +341,19 @@
282
341
  html = isError ? 'Execution failed' : 'Execution complete';
283
342
  }
284
343
  div.innerHTML = html;
344
+ if (resultText) {
345
+ var rereadBtn = document.createElement('button');
346
+ rereadBtn.className = 'voice-reread-btn';
347
+ rereadBtn.title = 'Re-read aloud';
348
+ rereadBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>';
349
+ rereadBtn.addEventListener('click', function() {
350
+ speak(resultText);
351
+ });
352
+ div.appendChild(rereadBtn);
353
+ }
285
354
  container.appendChild(div);
286
355
  scrollVoiceToBottom();
287
- if (ttsEnabled && resultText) {
356
+ if (autoSpeak && ttsEnabled && resultText) {
288
357
  speak(resultText);
289
358
  }
290
359
  return div;
@@ -305,31 +374,33 @@
305
374
  var data = e.detail;
306
375
  if (!data) return;
307
376
  if (data.type === 'streaming_progress' && data.block) {
308
- handleVoiceBlock(data.block);
377
+ handleVoiceBlock(data.block, true);
309
378
  }
310
379
  if (data.type === 'streaming_start') {
311
- lastSpokenBlockIndex = -1;
380
+ spokenChunks = new Set();
312
381
  }
313
382
  });
314
383
  window.addEventListener('conversation-selected', function(e) {
315
384
  currentConversationId = e.detail.conversationId;
385
+ stopSpeaking();
386
+ spokenChunks = new Set();
316
387
  if (voiceActive) {
317
388
  loadVoiceBlocks(currentConversationId);
318
389
  }
319
390
  });
320
391
  }
321
392
 
322
- function handleVoiceBlock(block) {
393
+ function handleVoiceBlock(block, isNew) {
323
394
  if (!block || !block.type) return;
324
395
  if (block.type === 'text' && block.text) {
325
396
  var div = addVoiceBlock(block.text, false);
326
- if (div && ttsEnabled) {
397
+ if (div && isNew && ttsEnabled) {
327
398
  div.classList.add('speaking');
328
399
  speak(block.text);
329
400
  setTimeout(function() { div.classList.remove('speaking'); }, 2000);
330
401
  }
331
402
  } else if (block.type === 'result') {
332
- addVoiceResultBlock(block);
403
+ addVoiceResultBlock(block, isNew);
333
404
  }
334
405
  }
335
406
 
@@ -341,9 +412,11 @@
341
412
  showVoiceEmpty(container);
342
413
  return;
343
414
  }
415
+ isLoadingHistory = true;
344
416
  fetch(BASE + '/api/conversations/' + conversationId + '/chunks')
345
417
  .then(function(res) { return res.json(); })
346
418
  .then(function(data) {
419
+ isLoadingHistory = false;
347
420
  if (!data.ok || !Array.isArray(data.chunks) || data.chunks.length === 0) {
348
421
  showVoiceEmpty(container);
349
422
  return;
@@ -356,19 +429,20 @@
356
429
  addVoiceBlock(block.text, false);
357
430
  hasContent = true;
358
431
  } else if (block.type === 'result') {
359
- addVoiceResultBlock(block);
432
+ addVoiceResultBlock(block, false);
360
433
  hasContent = true;
361
434
  }
362
435
  });
363
436
  if (!hasContent) showVoiceEmpty(container);
364
437
  })
365
438
  .catch(function() {
439
+ isLoadingHistory = false;
366
440
  showVoiceEmpty(container);
367
441
  });
368
442
  }
369
443
 
370
444
  function showVoiceEmpty(container) {
371
- container.innerHTML = '<div class="voice-empty"><div class="voice-empty-icon"><svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></div><div>Tap the microphone and speak to send a message.<br>Responses will be read aloud.</div></div>';
445
+ container.innerHTML = '<div class="voice-empty"><div class="voice-empty-icon"><svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></div><div>Hold the microphone button to record.<br>Release to transcribe. Tap Send to submit.<br>New responses will be read aloud.</div></div>';
372
446
  }
373
447
 
374
448
  function activate() {
package/static/styles.css CHANGED
@@ -281,10 +281,10 @@ html, body {
281
281
  .chat-messages {
282
282
  flex: 1;
283
283
  overflow-y: auto;
284
- padding: 2rem;
284
+ padding: 1rem 2rem;
285
285
  display: flex;
286
286
  flex-direction: column;
287
- gap: 1.5rem;
287
+ gap: 0.5rem;
288
288
  }
289
289
 
290
290
  .chat-context-header {
@@ -593,7 +593,7 @@ html, body {
593
593
  .message {
594
594
  display: flex;
595
595
  flex-wrap: wrap;
596
- gap: 1rem;
596
+ gap: 0.375rem;
597
597
  animation: slideUp 0.2s ease;
598
598
  width: 100%;
599
599
  }
@@ -631,7 +631,7 @@ html, body {
631
631
  max-width: 70%;
632
632
  display: flex;
633
633
  flex-direction: column;
634
- gap: 0.5rem;
634
+ gap: 0.25rem;
635
635
  }
636
636
 
637
637
  .stream-text-block {
@@ -788,7 +788,7 @@ html, body {
788
788
  border-radius: 0.5rem;
789
789
  overflow: visible;
790
790
  background: var(--bg-secondary);
791
- margin: 0.75rem 0;
791
+ margin: 0.25rem 0;
792
792
  }
793
793
 
794
794
  .html-header {
@@ -810,7 +810,7 @@ html, body {
810
810
  border-radius: 0.5rem;
811
811
  overflow-x: auto;
812
812
  background: var(--bg-tertiary);
813
- margin: 0.75rem 0;
813
+ margin: 0.25rem 0;
814
814
  font-family: 'Monaco', 'Courier New', monospace;
815
815
  font-size: 0.8rem;
816
816
  }
@@ -828,7 +828,7 @@ html, body {
828
828
  .collapsible-code {
829
829
  border-radius: 0.375rem;
830
830
  overflow: hidden;
831
- margin: 0.5rem 0;
831
+ margin: 0.25rem 0;
832
832
  border: 1px solid #334155;
833
833
  background: #1e293b;
834
834
  }
@@ -1271,7 +1271,7 @@ html, body {
1271
1271
  }
1272
1272
 
1273
1273
  .chat-messages {
1274
- padding: 1.5rem;
1274
+ padding: 0.75rem 1rem;
1275
1275
  }
1276
1276
 
1277
1277
  .chat-input-section {
@@ -1363,8 +1363,8 @@ html, body {
1363
1363
  /* Small height viewports */
1364
1364
  @media (max-height: 600px) {
1365
1365
  .chat-messages {
1366
- padding: 1rem;
1367
- gap: 0.75rem;
1366
+ padding: 0.75rem;
1367
+ gap: 0.375rem;
1368
1368
  }
1369
1369
 
1370
1370
  .chat-input-section {
@@ -1421,8 +1421,8 @@ html, body {
1421
1421
 
1422
1422
  @media (max-height: 500px) {
1423
1423
  .chat-messages {
1424
- padding: 0.75rem;
1425
- gap: 0.5rem;
1424
+ padding: 0.5rem;
1425
+ gap: 0.25rem;
1426
1426
  }
1427
1427
 
1428
1428
  .chat-input-section {
@@ -1663,8 +1663,8 @@ p code {
1663
1663
  .segment-tool-use {
1664
1664
  background: #f0f8ff;
1665
1665
  border-left: 4px solid #007acc;
1666
- padding: 1rem;
1667
- margin: 1rem 0;
1666
+ padding: 0.5rem;
1667
+ margin: 0.25rem 0;
1668
1668
  border-radius: 4px;
1669
1669
  }
1670
1670
 
@@ -1690,8 +1690,8 @@ p code {
1690
1690
  .segment-tool-result {
1691
1691
  background: #fff9e6;
1692
1692
  border-left: 4px solid #ffb300;
1693
- padding: 1rem;
1694
- margin: 1rem 0;
1693
+ padding: 0.5rem;
1694
+ margin: 0.25rem 0;
1695
1695
  border-radius: 4px;
1696
1696
  }
1697
1697
 
@@ -1744,12 +1744,12 @@ p code {
1744
1744
  .execution-blocks {
1745
1745
  display: flex;
1746
1746
  flex-direction: column;
1747
- gap: 0.75rem;
1747
+ gap: 0.25rem;
1748
1748
  width: 100%;
1749
1749
  }
1750
1750
 
1751
1751
  .message-block {
1752
- padding: 0.75rem 1rem;
1752
+ padding: 0.5rem 0.75rem;
1753
1753
  border-radius: 0.5rem;
1754
1754
  background: var(--bg-secondary);
1755
1755
  word-wrap: break-word;
@@ -1819,7 +1819,11 @@ p code {
1819
1819
  .block-tool-use {
1820
1820
  background: rgba(59, 130, 246, 0.05);
1821
1821
  border-radius: 0.5rem;
1822
- padding: 0.75rem;
1822
+ padding: 0.5rem;
1823
+ }
1824
+ .block-tool-use.folded-tool {
1825
+ padding: 0;
1826
+ background: none;
1823
1827
  }
1824
1828
 
1825
1829
  .tool-name {
@@ -1858,7 +1862,7 @@ p code {
1858
1862
  .block-tool-result {
1859
1863
  background: rgba(16, 185, 129, 0.05);
1860
1864
  border-radius: 0.5rem;
1861
- padding: 0.75rem;
1865
+ padding: 0.5rem;
1862
1866
  }
1863
1867
 
1864
1868
  .block-tool-result strong {