agentgui 1.0.253 → 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.253",
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);
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)) {
@@ -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