agentgui 1.0.252 → 1.0.254

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.252",
3
+ "version": "1.0.254",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -306,6 +306,7 @@ const discoveredAgents = discoverAgents();
306
306
  const modelCache = new Map();
307
307
 
308
308
  const AGENT_MODEL_COMMANDS = {
309
+ 'claude-code': 'claude models',
309
310
  'opencode': 'opencode models',
310
311
  'kilo': 'kilo models',
311
312
  };
@@ -317,6 +318,7 @@ const AGENT_DEFAULT_MODELS = {
317
318
  { id: 'opus', label: 'Opus' },
318
319
  { id: 'haiku', label: 'Haiku' },
319
320
  { id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5' },
321
+ { id: 'claude-sonnet-4-6-20260219', label: 'Sonnet 4.6' },
320
322
  { id: 'claude-opus-4-6', label: 'Opus 4.6' },
321
323
  { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' }
322
324
  ],
@@ -390,25 +392,25 @@ async function getModelsForAgent(agentId) {
390
392
  return models;
391
393
  }
392
394
 
393
- if (AGENT_DEFAULT_MODELS[agentId]) {
394
- const models = AGENT_DEFAULT_MODELS[agentId];
395
- modelCache.set(agentId, { models, timestamp: Date.now() });
396
- return models;
397
- }
398
-
399
395
  if (AGENT_MODEL_COMMANDS[agentId]) {
400
396
  try {
401
397
  const result = execSync(AGENT_MODEL_COMMANDS[agentId], { encoding: 'utf-8', timeout: 15000 });
402
398
  const lines = result.split('\n').map(l => l.trim()).filter(Boolean);
403
- const models = [{ id: '', label: 'Default' }];
404
- for (const line of lines) {
405
- models.push({ id: line, label: line });
399
+ if (lines.length > 0) {
400
+ const models = [{ id: '', label: 'Default' }];
401
+ for (const line of lines) {
402
+ models.push({ id: line, label: line });
403
+ }
404
+ modelCache.set(agentId, { models, timestamp: Date.now() });
405
+ return models;
406
406
  }
407
- modelCache.set(agentId, { models, timestamp: Date.now() });
408
- return models;
409
- } catch (_) {
410
- return [{ id: '', label: 'Default' }];
411
- }
407
+ } catch (_) {}
408
+ }
409
+
410
+ if (AGENT_DEFAULT_MODELS[agentId]) {
411
+ const models = AGENT_DEFAULT_MODELS[agentId];
412
+ modelCache.set(agentId, { models, timestamp: Date.now() });
413
+ return models;
412
414
  }
413
415
 
414
416
  const { getRegisteredAgents } = await import('./lib/claude-runner.js');
@@ -1970,6 +1972,41 @@ const server = http.createServer(async (req, res) => {
1970
1972
  return;
1971
1973
  }
1972
1974
 
1975
+ const agentUpdateMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/update$/);
1976
+ if (agentUpdateMatch && req.method === 'POST') {
1977
+ const agentId = agentUpdateMatch[1];
1978
+ const updateCommands = {
1979
+ 'claude-code': { cmd: 'claude', args: ['update', '--yes'] },
1980
+ };
1981
+ const updateCmd = updateCommands[agentId];
1982
+ if (!updateCmd) { sendJSON(req, res, 400, { error: 'No update command for this agent' }); return; }
1983
+ const conversationId = '__agent_update__';
1984
+ if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Update already running' }); return; }
1985
+ const child = spawn(updateCmd.cmd, updateCmd.args, {
1986
+ stdio: ['pipe', 'pipe', 'pipe'],
1987
+ env: { ...process.env, FORCE_COLOR: '1' },
1988
+ shell: os.platform() === 'win32'
1989
+ });
1990
+ activeScripts.set(conversationId, { process: child, script: 'update-' + agentId, startTime: Date.now() });
1991
+ broadcastSync({ type: 'script_started', conversationId, script: 'update-' + agentId, agentId, timestamp: Date.now() });
1992
+ const onData = (stream) => (chunk) => {
1993
+ broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
1994
+ };
1995
+ child.stdout.on('data', onData('stdout'));
1996
+ child.stderr.on('data', onData('stderr'));
1997
+ child.on('error', (err) => {
1998
+ activeScripts.delete(conversationId);
1999
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
2000
+ });
2001
+ child.on('close', (code) => {
2002
+ activeScripts.delete(conversationId);
2003
+ modelCache.delete(agentId);
2004
+ broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
2005
+ });
2006
+ sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
2007
+ return;
2008
+ }
2009
+
1973
2010
  if (pathOnly === '/api/auth/configs' && req.method === 'GET') {
1974
2011
  const configs = getProviderConfigs();
1975
2012
  sendJSON(req, res, 200, configs);
@@ -2555,9 +2592,9 @@ const server = http.createServer(async (req, res) => {
2555
2592
  const isWindows = os.platform() === 'win32';
2556
2593
  const result = execSync('git remote get-url origin' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2557
2594
  const remoteUrl = result.trim();
2558
- const statusResult = execSync('git status --porcelain', { encoding: 'utf-8', cwd: STARTUP_CWD });
2595
+ const statusResult = execSync('git status --porcelain' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2559
2596
  const hasChanges = statusResult.trim().length > 0;
2560
- const unpushedResult = execSync('git rev-list --count --not --remotes 2>/dev/null', { encoding: 'utf-8', cwd: STARTUP_CWD });
2597
+ const unpushedResult = execSync('git rev-list --count --not --remotes' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2561
2598
  const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
2562
2599
  const ownsRemote = !remoteUrl.includes('github.com/') || remoteUrl.includes(process.env.GITHUB_USER || '');
2563
2600
  sendJSON(req, res, 200, { ownsRemote, hasChanges, hasUnpushed, remoteUrl });
@@ -2570,7 +2607,10 @@ const server = http.createServer(async (req, res) => {
2570
2607
  if (pathOnly === '/api/git/push' && req.method === 'POST') {
2571
2608
  try {
2572
2609
  const isWindows = os.platform() === 'win32';
2573
- execSync('git add -A && git commit -m "Auto-commit" && git push', { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2610
+ const gitCommand = isWindows
2611
+ ? 'git add -A & git commit -m "Auto-commit" & git push'
2612
+ : 'git add -A && git commit -m "Auto-commit" && git push';
2613
+ execSync(gitCommand, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2574
2614
  sendJSON(req, res, 200, { success: true });
2575
2615
  } catch (err) {
2576
2616
  sendJSON(req, res, 500, { error: err.message });
package/static/app.js CHANGED
@@ -678,8 +678,8 @@ function closeNewChatModal() {
678
678
  }
679
679
  }
680
680
 
681
- function createChatInWorkspace() {
682
- const title = prompt('Enter a title for the conversation:', 'New Conversation');
681
+ async function createChatInWorkspace() {
682
+ const title = await window.UIDialog.prompt('Enter a title for the conversation:', 'New Conversation', 'New Chat');
683
683
  if (title) {
684
684
  app.createConversation(title);
685
685
  closeNewChatModal();
package/static/index.html CHANGED
@@ -1753,11 +1753,11 @@
1753
1753
  display: flex;
1754
1754
  align-items: center;
1755
1755
  gap: 0.375rem;
1756
- padding: 0.3rem 0.625rem;
1756
+ padding: 0.4rem 0.75rem;
1757
1757
  cursor: pointer;
1758
1758
  user-select: none;
1759
1759
  list-style: none;
1760
- font-size: 0.75rem;
1760
+ font-size: 0.85rem;
1761
1761
  line-height: 1.3;
1762
1762
  background: #dcfce7;
1763
1763
  transition: background 0.15s;
@@ -1784,24 +1784,24 @@
1784
1784
  display: flex;
1785
1785
  align-items: center;
1786
1786
  color: #16a34a;
1787
- width: 0.875rem;
1788
- height: 0.875rem;
1787
+ width: 1rem;
1788
+ height: 1rem;
1789
1789
  flex-shrink: 0;
1790
1790
  }
1791
1791
  html.dark .folded-tool-icon { color: #4ade80; }
1792
- .folded-tool-icon svg { width: 0.875rem; height: 0.875rem; }
1792
+ .folded-tool-icon svg { width: 1rem; height: 1rem; }
1793
1793
  .folded-tool-name {
1794
1794
  font-weight: 600;
1795
1795
  color: #166534;
1796
1796
  font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
1797
- font-size: 0.7rem;
1797
+ font-size: 0.8rem;
1798
1798
  flex-shrink: 0;
1799
1799
  }
1800
1800
  html.dark .folded-tool-name { color: #86efac; }
1801
1801
  .folded-tool-desc {
1802
1802
  color: #15803d;
1803
1803
  font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
1804
- font-size: 0.7rem;
1804
+ font-size: 0.8rem;
1805
1805
  overflow: hidden;
1806
1806
  text-overflow: ellipsis;
1807
1807
  white-space: nowrap;
@@ -1874,6 +1874,33 @@
1874
1874
  .folded-tool-info > .folded-tool-body { border-top-color: #c7d2fe; }
1875
1875
  html.dark .folded-tool-info > .folded-tool-body { border-top-color: #3730a3; }
1876
1876
 
1877
+ /* --- Consecutive block joining --- */
1878
+ .folded-tool + .folded-tool,
1879
+ .block-tool-use + .block-tool-use {
1880
+ margin-top: -1px;
1881
+ border-radius: 0;
1882
+ }
1883
+ .folded-tool + .folded-tool > .folded-tool-bar,
1884
+ .block-tool-use + .block-tool-use > .folded-tool-bar {
1885
+ border-top: 1px solid rgba(0,0,0,0.06);
1886
+ }
1887
+ html.dark .folded-tool + .folded-tool > .folded-tool-bar,
1888
+ html.dark .block-tool-use + .block-tool-use > .folded-tool-bar {
1889
+ border-top: 1px solid rgba(255,255,255,0.06);
1890
+ }
1891
+ .folded-tool:first-child,
1892
+ .block-tool-use:first-child {
1893
+ border-radius: 0.375rem 0.375rem 0 0;
1894
+ }
1895
+ .folded-tool:last-child,
1896
+ .block-tool-use:last-child {
1897
+ border-radius: 0 0 0.375rem 0.375rem;
1898
+ }
1899
+ .folded-tool:only-child,
1900
+ .block-tool-use:only-child {
1901
+ border-radius: 0.375rem;
1902
+ }
1903
+
1877
1904
  /* --- Inline Tool Result (nested inside tool_use) --- */
1878
1905
  .tool-result-inline {
1879
1906
  border-top: 1px solid #bbf7d0;
@@ -1905,6 +1932,30 @@
1905
1932
  .tool-result-error > .folded-tool-body { border-top-color: #fecaca; }
1906
1933
  html.dark .tool-result-error > .folded-tool-body { border-top-color: #991b1b; }
1907
1934
 
1935
+ /* --- Consecutive Block Joining --- */
1936
+ .streaming-blocks > * + *,
1937
+ .message-blocks > * + * {
1938
+ margin-top: 0;
1939
+ }
1940
+ .streaming-blocks > *,
1941
+ .message-blocks > * {
1942
+ border-radius: 0;
1943
+ }
1944
+ .streaming-blocks > *:first-child,
1945
+ .message-blocks > *:first-child {
1946
+ border-top-left-radius: 0.375rem;
1947
+ border-top-right-radius: 0.375rem;
1948
+ }
1949
+ .streaming-blocks > *:last-child,
1950
+ .message-blocks > *:last-child {
1951
+ border-bottom-left-radius: 0.375rem;
1952
+ border-bottom-right-radius: 0.375rem;
1953
+ }
1954
+ .streaming-blocks > *:only-child,
1955
+ .message-blocks > *:only-child {
1956
+ border-radius: 0.375rem;
1957
+ }
1958
+
1908
1959
  /* --- Collapsible Code Summary --- */
1909
1960
  .collapsible-code {
1910
1961
  margin: 0;
@@ -315,14 +315,24 @@ class AgentGUIClient {
315
315
  const scrollContainer = document.getElementById(this.config.scrollContainerId);
316
316
  if (!scrollContainer) return;
317
317
 
318
+ this._userScrolledUp = false;
318
319
  let scrollTimer = null;
320
+ let lastScrollTop = scrollContainer.scrollTop;
319
321
  scrollContainer.addEventListener('scroll', () => {
322
+ const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
323
+ if (scrollContainer.scrollTop < lastScrollTop && distFromBottom > 200) {
324
+ this._userScrolledUp = true;
325
+ } else if (distFromBottom < 50) {
326
+ this._userScrolledUp = false;
327
+ this._removeNewContentPill();
328
+ }
329
+ lastScrollTop = scrollContainer.scrollTop;
320
330
  if (scrollTimer) clearTimeout(scrollTimer);
321
331
  scrollTimer = setTimeout(() => {
322
332
  if (this.state.currentConversation?.id) {
323
333
  this.saveScrollPosition(this.state.currentConversation.id);
324
334
  }
325
- }, 500); // Debounce 500ms
335
+ }, 500);
326
336
  });
327
337
  }
328
338
 
@@ -706,50 +716,21 @@ class AgentGUIClient {
706
716
  if (!scrollContainer) return;
707
717
  const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
708
718
 
709
- if (!force && distFromBottom > 150) {
719
+ if (this._userScrolledUp && !force) {
710
720
  this._unseenCount = (this._unseenCount || 0) + 1;
711
721
  this._showNewContentPill();
712
722
  return;
713
723
  }
714
724
 
715
- const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
716
- const isStreaming = this.state.streamingConversations.size > 0;
717
-
718
- if (!isStreaming || !this._scrollKalman || Math.abs(maxScroll - scrollContainer.scrollTop) > 2000 || force) {
719
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
720
- this._removeNewContentPill();
721
- this._scrollAnimating = false;
725
+ if (!force && distFromBottom > 150) {
726
+ this._unseenCount = (this._unseenCount || 0) + 1;
727
+ this._showNewContentPill();
722
728
  return;
723
729
  }
724
730
 
725
- this._scrollKalman.update(maxScroll);
726
- this._scrollTarget = this._scrollKalman.predict();
727
-
728
- const conf = this._chunkArrivalConfidence();
729
- if (conf > 0.5) {
730
- const estHeight = this._estimatedBlockHeight('text') * 0.5 * conf;
731
- this._scrollTarget += estHeight;
732
- const trueMax = scrollContainer.scrollHeight - scrollContainer.clientHeight;
733
- if (this._scrollTarget > trueMax + 100) this._scrollTarget = trueMax + 100;
734
- }
735
-
736
- if (!this._scrollAnimating) {
737
- this._scrollAnimating = true;
738
- const animate = () => {
739
- if (!this._scrollAnimating) return;
740
- const sc = document.getElementById('output-scroll');
741
- if (!sc) { this._scrollAnimating = false; return; }
742
- const diff = this._scrollTarget - sc.scrollTop;
743
- if (Math.abs(diff) < 1) {
744
- sc.scrollTop = this._scrollTarget;
745
- if (this.state.streamingConversations.size === 0) { this._scrollAnimating = false; return; }
746
- }
747
- sc.scrollTop += diff * this._scrollLerpFactor;
748
- this._removeNewContentPill();
749
- requestAnimationFrame(animate);
750
- };
751
- requestAnimationFrame(animate);
752
- }
731
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
732
+ this._removeNewContentPill();
733
+ this._scrollAnimating = false;
753
734
  }
754
735
 
755
736
  _showNewContentPill() {
@@ -1205,11 +1186,11 @@ class AgentGUIClient {
1205
1186
  const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
1206
1187
  const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
1207
1188
  const colorIdx = hasRenderer && this.renderer ? this.renderer._getBlockColorIndex('tool_use') : 1;
1208
- 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}`;
1189
+ html += `<details class="block-tool-use folded-tool" 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}`;
1209
1190
  pendingToolUseClose = true;
1210
1191
  } else if (block.type === 'tool_result') {
1211
1192
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1212
- const smartHtml = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.renderSmartContentHTML(content, this.escapeHtml.bind(this)) : `<pre class="tool-result-pre">${this.escapeHtml(content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content)}</pre>`;
1193
+ const smartHtml = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.renderSmartContentHTML(content, this.escapeHtml.bind(this), true) : `<pre class="tool-result-pre">${this.escapeHtml(content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content)}</pre>`;
1213
1194
  const resultPreview = content.length > 80 ? content.substring(0, 77).replace(/\n/g, ' ') + '...' : content.replace(/\n/g, ' ');
1214
1195
  const resultIcon = block.is_error
1215
1196
  ? '<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>'
@@ -2235,6 +2216,7 @@ class AgentGUIClient {
2235
2216
 
2236
2217
  this.cacheCurrentConversation();
2237
2218
  this.stopChunkPolling();
2219
+ if (this.renderer.resetScrollState) this.renderer.resetScrollState();
2238
2220
  var prevId = this.state.currentConversation?.id;
2239
2221
  if (prevId && prevId !== conversationId) {
2240
2222
  if (this.wsManager.isConnected && !this.state.streamingConversations.has(prevId)) {
@@ -475,7 +475,10 @@ class ConversationManager {
475
475
  }
476
476
 
477
477
  async confirmDelete(convId, title) {
478
- const confirmed = confirm(`Delete conversation "${title || 'Untitled'}"?\n\nThis will also delete any associated Claude Code session data. This action cannot be undone.`);
478
+ const confirmed = await window.UIDialog.confirm(
479
+ `Delete conversation "${title || 'Untitled'}"?\n\nThis will also delete any associated Claude Code session data. This action cannot be undone.`,
480
+ 'Delete Conversation'
481
+ );
479
482
  if (!confirmed) return;
480
483
 
481
484
  try {
@@ -486,15 +489,14 @@ class ConversationManager {
486
489
 
487
490
  if (res.ok) {
488
491
  console.log(`[ConversationManager] Deleted conversation ${convId}`);
489
- // Remove from local list immediately for responsive UI
490
492
  this.deleteConversation(convId);
491
493
  } else {
492
494
  const error = await res.json().catch(() => ({ error: 'Failed to delete' }));
493
- alert('Failed to delete conversation: ' + (error.error || 'Unknown error'));
495
+ window.UIDialog.alert('Failed to delete conversation: ' + (error.error || 'Unknown error'), 'Error');
494
496
  }
495
497
  } catch (err) {
496
498
  console.error('[ConversationManager] Delete error:', err);
497
- alert('Failed to delete conversation: ' + err.message);
499
+ window.UIDialog.alert('Failed to delete conversation: ' + err.message, 'Error');
498
500
  }
499
501
  }
500
502
 
@@ -0,0 +1,267 @@
1
+ (function() {
2
+ var activeDialogs = [];
3
+ var dialogZIndex = 10000;
4
+
5
+ function escapeHtml(text) {
6
+ var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
7
+ return String(text).replace(/[&<>"']/g, function(c) { return map[c]; });
8
+ }
9
+
10
+ function createOverlay() {
11
+ var overlay = document.createElement('div');
12
+ overlay.className = 'dialog-overlay';
13
+ overlay.innerHTML = '<div class="dialog-backdrop"></div>';
14
+ return overlay;
15
+ }
16
+
17
+ function showDialog(dialog, overlay) {
18
+ dialogZIndex++;
19
+ if (overlay) {
20
+ overlay.style.zIndex = dialogZIndex;
21
+ document.body.appendChild(overlay);
22
+ }
23
+ dialog.style.zIndex = dialogZIndex + 1;
24
+ document.body.appendChild(dialog);
25
+ activeDialogs.push({ dialog: dialog, overlay: overlay });
26
+
27
+ requestAnimationFrame(function() {
28
+ dialog.classList.add('visible');
29
+ if (overlay) overlay.classList.add('visible');
30
+ var input = dialog.querySelector('input, textarea');
31
+ if (input) input.focus();
32
+ else {
33
+ var btn = dialog.querySelector('.dialog-btn-primary');
34
+ if (btn) btn.focus();
35
+ }
36
+ });
37
+ }
38
+
39
+ function closeDialog(dialog, overlay) {
40
+ dialog.classList.remove('visible');
41
+ if (overlay) overlay.classList.remove('visible');
42
+ setTimeout(function() {
43
+ if (dialog.parentNode) dialog.remove();
44
+ if (overlay && overlay.parentNode) overlay.remove();
45
+ }, 200);
46
+ activeDialogs = activeDialogs.filter(function(d) {
47
+ return d.dialog !== dialog;
48
+ });
49
+ }
50
+
51
+ function closeAllDialogs() {
52
+ activeDialogs.forEach(function(d) {
53
+ closeDialog(d.dialog, d.overlay);
54
+ });
55
+ }
56
+
57
+ window.UIDialog = {
58
+ alert: function(message, title) {
59
+ return new Promise(function(resolve) {
60
+ var overlay = createOverlay();
61
+ var dialog = document.createElement('div');
62
+ dialog.className = 'dialog-container';
63
+ dialog.innerHTML =
64
+ '<div class="dialog-box">' +
65
+ '<div class="dialog-header">' +
66
+ '<h3 class="dialog-title">' + escapeHtml(title || 'Alert') + '</h3>' +
67
+ '</div>' +
68
+ '<div class="dialog-body">' +
69
+ '<p class="dialog-message">' + escapeHtml(message) + '</p>' +
70
+ '</div>' +
71
+ '<div class="dialog-footer">' +
72
+ '<button class="dialog-btn dialog-btn-primary" data-action="ok">OK</button>' +
73
+ '</div>' +
74
+ '</div>';
75
+
76
+ var okBtn = dialog.querySelector('[data-action="ok"]');
77
+ okBtn.addEventListener('click', function() {
78
+ closeDialog(dialog, overlay);
79
+ resolve(true);
80
+ });
81
+
82
+ overlay.querySelector('.dialog-backdrop').addEventListener('click', function() {
83
+ closeDialog(dialog, overlay);
84
+ resolve(true);
85
+ });
86
+
87
+ document.addEventListener('keydown', function handler(e) {
88
+ if (e.key === 'Escape' || e.key === 'Enter') {
89
+ document.removeEventListener('keydown', handler);
90
+ closeDialog(dialog, overlay);
91
+ resolve(true);
92
+ }
93
+ });
94
+
95
+ showDialog(dialog, overlay);
96
+ });
97
+ },
98
+
99
+ confirm: function(message, title) {
100
+ return new Promise(function(resolve) {
101
+ var overlay = createOverlay();
102
+ var dialog = document.createElement('div');
103
+ dialog.className = 'dialog-container';
104
+ dialog.innerHTML =
105
+ '<div class="dialog-box">' +
106
+ '<div class="dialog-header">' +
107
+ '<h3 class="dialog-title">' + escapeHtml(title || 'Confirm') + '</h3>' +
108
+ '</div>' +
109
+ '<div class="dialog-body">' +
110
+ '<p class="dialog-message">' + escapeHtml(message).replace(/\n/g, '<br>') + '</p>' +
111
+ '</div>' +
112
+ '<div class="dialog-footer">' +
113
+ '<button class="dialog-btn dialog-btn-secondary" data-action="cancel">Cancel</button>' +
114
+ '<button class="dialog-btn dialog-btn-primary dialog-btn-danger" data-action="confirm">Confirm</button>' +
115
+ '</div>' +
116
+ '</div>';
117
+
118
+ var cancelBtn = dialog.querySelector('[data-action="cancel"]');
119
+ var confirmBtn = dialog.querySelector('[data-action="confirm"]');
120
+
121
+ cancelBtn.addEventListener('click', function() {
122
+ closeDialog(dialog, overlay);
123
+ resolve(false);
124
+ });
125
+
126
+ confirmBtn.addEventListener('click', function() {
127
+ closeDialog(dialog, overlay);
128
+ resolve(true);
129
+ });
130
+
131
+ overlay.querySelector('.dialog-backdrop').addEventListener('click', function() {
132
+ closeDialog(dialog, overlay);
133
+ resolve(false);
134
+ });
135
+
136
+ document.addEventListener('keydown', function handler(e) {
137
+ if (e.key === 'Escape') {
138
+ document.removeEventListener('keydown', handler);
139
+ closeDialog(dialog, overlay);
140
+ resolve(false);
141
+ } else if (e.key === 'Enter') {
142
+ document.removeEventListener('keydown', handler);
143
+ closeDialog(dialog, overlay);
144
+ resolve(true);
145
+ }
146
+ });
147
+
148
+ showDialog(dialog, overlay);
149
+ });
150
+ },
151
+
152
+ prompt: function(message, defaultValue, title) {
153
+ return new Promise(function(resolve) {
154
+ var overlay = createOverlay();
155
+ var dialog = document.createElement('div');
156
+ dialog.className = 'dialog-container';
157
+ dialog.innerHTML =
158
+ '<div class="dialog-box">' +
159
+ '<div class="dialog-header">' +
160
+ '<h3 class="dialog-title">' + escapeHtml(title || 'Input') + '</h3>' +
161
+ '</div>' +
162
+ '<div class="dialog-body">' +
163
+ '<label class="dialog-label">' + escapeHtml(message) + '</label>' +
164
+ '<input type="text" class="dialog-input" value="' + escapeHtml(defaultValue || '') + '">' +
165
+ '</div>' +
166
+ '<div class="dialog-footer">' +
167
+ '<button class="dialog-btn dialog-btn-secondary" data-action="cancel">Cancel</button>' +
168
+ '<button class="dialog-btn dialog-btn-primary" data-action="ok">OK</button>' +
169
+ '</div>' +
170
+ '</div>';
171
+
172
+ var input = dialog.querySelector('.dialog-input');
173
+ var cancelBtn = dialog.querySelector('[data-action="cancel"]');
174
+ var okBtn = dialog.querySelector('[data-action="ok"]');
175
+
176
+ cancelBtn.addEventListener('click', function() {
177
+ closeDialog(dialog, overlay);
178
+ resolve(null);
179
+ });
180
+
181
+ okBtn.addEventListener('click', function() {
182
+ closeDialog(dialog, overlay);
183
+ resolve(input.value);
184
+ });
185
+
186
+ input.addEventListener('keydown', function(e) {
187
+ if (e.key === 'Enter') {
188
+ closeDialog(dialog, overlay);
189
+ resolve(input.value);
190
+ }
191
+ });
192
+
193
+ overlay.querySelector('.dialog-backdrop').addEventListener('click', function() {
194
+ closeDialog(dialog, overlay);
195
+ resolve(null);
196
+ });
197
+
198
+ document.addEventListener('keydown', function handler(e) {
199
+ if (e.key === 'Escape') {
200
+ document.removeEventListener('keydown', handler);
201
+ closeDialog(dialog, overlay);
202
+ resolve(null);
203
+ }
204
+ });
205
+
206
+ showDialog(dialog, overlay);
207
+ });
208
+ },
209
+
210
+ showProgress: function(config) {
211
+ var overlay = createOverlay();
212
+ var dialog = document.createElement('div');
213
+ dialog.className = 'dialog-container';
214
+ dialog.innerHTML =
215
+ '<div class="dialog-box dialog-box-progress">' +
216
+ '<div class="dialog-header">' +
217
+ '<h3 class="dialog-title">' + escapeHtml(config.title || 'Please wait') + '</h3>' +
218
+ '</div>' +
219
+ '<div class="dialog-body">' +
220
+ '<p class="dialog-message progress-message">' + escapeHtml(config.message || 'Loading...') + '</p>' +
221
+ '<div class="dialog-progress-bar">' +
222
+ '<div class="dialog-progress-fill" style="width: 0%"></div>' +
223
+ '</div>' +
224
+ '<p class="dialog-progress-percent">0%</p>' +
225
+ '</div>' +
226
+ '</div>';
227
+
228
+ showDialog(dialog, overlay);
229
+
230
+ var progressFill = dialog.querySelector('.dialog-progress-fill');
231
+ var progressPercent = dialog.querySelector('.dialog-progress-percent');
232
+ var progressMessage = dialog.querySelector('.progress-message');
233
+
234
+ return {
235
+ update: function(percent, message) {
236
+ progressFill.style.width = percent + '%';
237
+ progressPercent.textContent = Math.round(percent) + '%';
238
+ if (message) progressMessage.textContent = message;
239
+ },
240
+ close: function() {
241
+ closeDialog(dialog, overlay);
242
+ }
243
+ };
244
+ },
245
+
246
+ showToast: function(message, type, duration) {
247
+ var existing = document.querySelector('.toast-notification');
248
+ if (existing) existing.remove();
249
+
250
+ var toast = document.createElement('div');
251
+ toast.className = 'toast-notification toast-' + (type || 'info');
252
+ toast.innerHTML = '<span class="toast-message">' + escapeHtml(message) + '</span>';
253
+ document.body.appendChild(toast);
254
+
255
+ requestAnimationFrame(function() {
256
+ toast.classList.add('visible');
257
+ });
258
+
259
+ setTimeout(function() {
260
+ toast.classList.remove('visible');
261
+ setTimeout(function() { if (toast.parentNode) toast.remove(); }, 300);
262
+ }, duration || 3000);
263
+ },
264
+
265
+ closeAll: closeAllDialogs
266
+ };
267
+ })();
@@ -123,7 +123,7 @@
123
123
  overlay.classList.remove('visible');
124
124
 
125
125
  if (!currentConversation) {
126
- showToast('Select a conversation first', 'error');
126
+ if (window.UIDialog) window.UIDialog.showToast('Select a conversation first', 'error');
127
127
  return;
128
128
  }
129
129
 
@@ -136,7 +136,7 @@
136
136
 
137
137
  function uploadFiles(files) {
138
138
  if (!currentConversation) {
139
- showToast('No conversation selected', 'error');
139
+ if (window.UIDialog) window.UIDialog.showToast('No conversation selected', 'error');
140
140
  return;
141
141
  }
142
142
 
@@ -145,7 +145,7 @@
145
145
  formData.append('file', files[i]);
146
146
  }
147
147
 
148
- showToast('Uploading ' + files.length + ' file(s)...', 'info');
148
+ if (window.UIDialog) window.UIDialog.showToast('Uploading ' + files.length + ' file(s)...', 'info');
149
149
 
150
150
  fetch(BASE + '/api/upload/' + currentConversation, {
151
151
  method: 'POST',
@@ -154,13 +154,13 @@
154
154
  .then(function(res) { return res.json(); })
155
155
  .then(function(data) {
156
156
  if (data.ok) {
157
- showToast(data.count + ' file(s) uploaded', 'success');
157
+ if (window.UIDialog) window.UIDialog.showToast(data.count + ' file(s) uploaded', 'success');
158
158
  } else {
159
- showToast('Upload failed: ' + (data.error || 'Unknown error'), 'error');
159
+ if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + (data.error || 'Unknown error'), 'error');
160
160
  }
161
161
  })
162
162
  .catch(function(err) {
163
- showToast('Upload failed: ' + err.message, 'error');
163
+ if (window.UIDialog) window.UIDialog.showToast('Upload failed: ' + err.message, 'error');
164
164
  });
165
165
  }
166
166
 
@@ -197,6 +197,42 @@
197
197
  });
198
198
  }
199
199
 
200
+ function showVoiceDownloadProgress() {
201
+ if (window._voiceProgressDialog) return;
202
+
203
+ window._voiceProgressDialog = window.UIDialog.showProgress({
204
+ title: 'Downloading Voice Models',
205
+ message: 'Preparing speech recognition and synthesis models...'
206
+ });
207
+
208
+ var checkInterval = setInterval(function() {
209
+ if (isVoiceReady()) {
210
+ clearInterval(checkInterval);
211
+ if (window._voiceProgressDialog) {
212
+ window._voiceProgressDialog.close();
213
+ window._voiceProgressDialog = null;
214
+ }
215
+ switchView('voice');
216
+ }
217
+ }, 500);
218
+
219
+ setTimeout(function() {
220
+ clearInterval(checkInterval);
221
+ if (window._voiceProgressDialog) {
222
+ window._voiceProgressDialog.close();
223
+ window._voiceProgressDialog = null;
224
+ }
225
+ }, 120000);
226
+ }
227
+
228
+ function updateVoiceProgress(percent, message) {
229
+ if (window._voiceProgressDialog) {
230
+ window._voiceProgressDialog.update(percent, message);
231
+ }
232
+ }
233
+
234
+ window.__updateVoiceProgress = updateVoiceProgress;
235
+
200
236
  function isVoiceReady() {
201
237
  var client = window.agentGUIClient;
202
238
  if (!client) return false;
@@ -98,6 +98,14 @@ class StreamingRenderer {
98
98
  * Setup scroll optimization and auto-scroll
99
99
  */
100
100
  setupScrollOptimization() {
101
+ if (!this.scrollContainer) return;
102
+ this._userScrolledUp = false;
103
+ this.scrollContainer.addEventListener('scroll', () => {
104
+ if (this._programmaticScroll) return;
105
+ const sc = this.scrollContainer;
106
+ const distFromBottom = sc.scrollHeight - sc.scrollTop - sc.clientHeight;
107
+ this._userScrolledUp = distFromBottom > 80;
108
+ });
101
109
  }
102
110
 
103
111
  /**
@@ -749,7 +757,6 @@ class StreamingRenderer {
749
757
 
750
758
  const details = document.createElement('details');
751
759
  details.className = 'block-tool-use folded-tool';
752
- details.setAttribute('open', '');
753
760
  if (block.id) details.dataset.toolUseId = block.id;
754
761
  const colorIndex = this._getBlockColorIndex('tool_use');
755
762
  details.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
@@ -856,7 +863,7 @@ class StreamingRenderer {
856
863
  /**
857
864
  * Static HTML version of smart content rendering for use in string templates
858
865
  */
859
- static renderSmartContentHTML(contentStr, escapeHtml) {
866
+ static renderSmartContentHTML(contentStr, escapeHtml, flat = false) {
860
867
  const trimmed = contentStr.trim();
861
868
  const esc = escapeHtml || window._escHtml;
862
869
 
@@ -874,8 +881,7 @@ class StreamingRenderer {
874
881
  const textParts = parsed.filter(b => b.type === 'text' && b.text);
875
882
  if (textParts.length > 0) {
876
883
  const combined = textParts.map(b => b.text).join('\n');
877
- // Re-enter renderSmartContentHTML with the extracted text
878
- return StreamingRenderer.renderSmartContentHTML(combined, esc);
884
+ return StreamingRenderer.renderSmartContentHTML(combined, esc, flat);
879
885
  }
880
886
  }
881
887
 
@@ -905,7 +911,7 @@ class StreamingRenderer {
905
911
  const cleanedContent = cleanedLines.join('\n');
906
912
 
907
913
  // Try to detect and highlight code based on content patterns
908
- return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc);
914
+ return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc, flat);
909
915
  }
910
916
 
911
917
  // Check for system reminder tags and format them specially
@@ -1001,7 +1007,7 @@ class StreamingRenderer {
1001
1007
 
1002
1008
  // Add highlighted code
1003
1009
  if (codeContent.trim()) {
1004
- html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc);
1010
+ html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc, flat);
1005
1011
  }
1006
1012
 
1007
1013
  // Add system reminders if any
@@ -1021,7 +1027,7 @@ class StreamingRenderer {
1021
1027
  if (contentWithoutReminders) {
1022
1028
  // Check if remaining content looks like code
1023
1029
  if (StreamingRenderer.detectCodeContent(contentWithoutReminders)) {
1024
- html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc);
1030
+ html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc, flat);
1025
1031
  } else {
1026
1032
  html += `<pre class="tool-result-pre">${esc(contentWithoutReminders)}</pre>`;
1027
1033
  }
@@ -1050,7 +1056,7 @@ class StreamingRenderer {
1050
1056
  // Check if this looks like code
1051
1057
  const looksLikeCode = StreamingRenderer.detectCodeContent(trimmed);
1052
1058
  if (looksLikeCode) {
1053
- return StreamingRenderer.renderCodeWithHighlight(trimmed, esc);
1059
+ return StreamingRenderer.renderCodeWithHighlight(trimmed, esc, flat);
1054
1060
  }
1055
1061
 
1056
1062
  const displayContent = trimmed.length > 2000 ? trimmed.substring(0, 2000) + '\n... (truncated)' : trimmed;
@@ -1109,11 +1115,12 @@ class StreamingRenderer {
1109
1115
  /**
1110
1116
  * Render code with basic syntax highlighting
1111
1117
  */
1112
- static renderCodeWithHighlight(code, esc) {
1113
- const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;border-top:none;margin:0";
1118
+ static renderCodeWithHighlight(code, esc, flat = false) {
1119
+ const preStyle = "background:#1e293b;padding:1rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;margin:0";
1120
+ const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
1121
+ if (flat) return codeHtml;
1114
1122
  const lineCount = code.split('\n').length;
1115
1123
  const summaryLabel = `code - ${lineCount} line${lineCount !== 1 ? 's' : ''}`;
1116
- const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
1117
1124
  return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
1118
1125
  }
1119
1126
 
@@ -1240,7 +1247,7 @@ class StreamingRenderer {
1240
1247
  `;
1241
1248
  wrapper.appendChild(header);
1242
1249
 
1243
- const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this));
1250
+ const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
1244
1251
  const body = document.createElement('div');
1245
1252
  body.className = 'folded-tool-body';
1246
1253
  if (!parentIsOpen) {
@@ -1348,7 +1355,6 @@ class StreamingRenderer {
1348
1355
  const details = document.createElement('details');
1349
1356
  details.className = isError ? 'folded-tool folded-tool-error' : 'folded-tool';
1350
1357
  details.dataset.eventType = 'result';
1351
- if (!isError) details.setAttribute('open', '');
1352
1358
  const colorIndex = this._getBlockColorIndex(isError ? 'error' : 'result');
1353
1359
  details.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
1354
1360
 
@@ -2003,16 +2009,22 @@ class StreamingRenderer {
2003
2009
  * Auto-scroll to bottom of container
2004
2010
  */
2005
2011
  autoScroll() {
2006
- if (this._scrollRafPending) return;
2012
+ if (this._scrollRafPending || this._userScrolledUp) return;
2007
2013
  this._scrollRafPending = true;
2008
2014
  requestAnimationFrame(() => {
2009
2015
  this._scrollRafPending = false;
2010
2016
  if (this.scrollContainer) {
2017
+ this._programmaticScroll = true;
2011
2018
  try { this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; } catch (_) {}
2019
+ this._programmaticScroll = false;
2012
2020
  }
2013
2021
  });
2014
2022
  }
2015
2023
 
2024
+ resetScrollState() {
2025
+ this._userScrolledUp = false;
2026
+ }
2027
+
2016
2028
  updateVirtualScroll() {
2017
2029
  }
2018
2030