agentgui 1.0.714 → 1.0.715

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.
@@ -4,7 +4,6 @@ import os from 'os';
4
4
 
5
5
  const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
6
6
  const DEBOUNCE_MS = 16;
7
- const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
8
7
 
9
8
  export class JsonlWatcher {
10
9
  constructor({ broadcastSync, queries, ownedSessionIds }) {
@@ -16,12 +15,13 @@ export class JsonlWatcher {
16
15
  this._frags = new Map();
17
16
  this._timers = new Map();
18
17
  this._seqs = new Map();
18
+ this._streaming = new Set();
19
19
  this._watcher = null;
20
20
  }
21
21
 
22
22
  start() {
23
23
  if (!fs.existsSync(PROJECTS_DIR)) return;
24
- this._scan();
24
+ this._scanDir(PROJECTS_DIR, 0);
25
25
  try {
26
26
  this._watcher = fs.watch(PROJECTS_DIR, { recursive: true }, (_, f) => {
27
27
  if (f && f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, f));
@@ -41,34 +41,32 @@ export class JsonlWatcher {
41
41
  for (const sid of sids) {
42
42
  this._convMap.delete(sid);
43
43
  this._seqs.delete(sid);
44
+ this._streaming.delete(sid);
44
45
  for (const key of [...this._frags.keys()]) if (key.startsWith(`${sid}:`)) this._frags.delete(key);
45
- const fp = path.join(PROJECTS_DIR, sid + '.jsonl');
46
- const s = this._tails.get(fp);
47
- if (s?.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
48
- this._tails.delete(fp);
49
- const t = this._timers.get(fp);
50
- if (t) { clearTimeout(t); this._timers.delete(fp); }
46
+ for (const [fp, s] of this._tails.entries()) {
47
+ if (!fp.includes(sid)) continue;
48
+ if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
49
+ this._tails.delete(fp);
50
+ const t = this._timers.get(fp);
51
+ if (t) { clearTimeout(t); this._timers.delete(fp); }
52
+ }
51
53
  }
52
54
  }
53
55
 
54
56
  removeAllConversations() {
55
57
  for (const s of this._tails.values()) if (s.fd !== null) try { fs.closeSync(s.fd); } catch (_) {}
56
58
  for (const t of this._timers.values()) clearTimeout(t);
57
- this._tails.clear();
58
- this._convMap.clear();
59
- this._frags.clear();
60
- this._timers.clear();
61
- this._seqs.clear();
59
+ this._tails.clear(); this._convMap.clear(); this._frags.clear();
60
+ this._timers.clear(); this._seqs.clear(); this._streaming.clear();
62
61
  }
63
62
 
64
- _scan() {
63
+ _scanDir(dir, depth) {
64
+ if (depth > 4) return;
65
65
  try {
66
- for (const d of fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
67
- if (!d.isDirectory()) continue;
68
- try {
69
- for (const f of fs.readdirSync(path.join(PROJECTS_DIR, d.name)))
70
- if (f.endsWith('.jsonl')) this._debounce(path.join(PROJECTS_DIR, d.name, f));
71
- } catch (_) {}
66
+ for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
67
+ const fp = path.join(dir, d.name);
68
+ if (d.isFile() && d.name.endsWith('.jsonl')) this._debounce(fp);
69
+ else if (d.isDirectory()) this._scanDir(fp, depth + 1);
72
70
  }
73
71
  } catch (_) {}
74
72
  }
@@ -112,7 +110,7 @@ export class JsonlWatcher {
112
110
  if (cid) this._route(cid, e.sessionId, e);
113
111
  }
114
112
 
115
- _conv(sid, e, fp) {
113
+ _conv(sid, e) {
116
114
  if (this._convMap.has(sid)) return this._convMap.get(sid);
117
115
  const found = this._q.getConversations().find(c => c.claudeSessionId === sid);
118
116
  if (found) { this._convMap.set(sid, found.id); return found.id; }
@@ -130,39 +128,67 @@ export class JsonlWatcher {
130
128
 
131
129
  _seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
132
130
 
131
+ _emit(cid, sid, block, role, extra) {
132
+ this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block, blockRole: role, seq: this._seq(sid), timestamp: Date.now(), ...extra });
133
+ }
134
+
135
+ _startStreaming(cid, sid) {
136
+ if (this._streaming.has(sid)) return;
137
+ this._streaming.add(sid);
138
+ this._bc({ type: 'streaming_start', sessionId: sid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() });
139
+ }
140
+
141
+ _endStreaming(cid, sid) {
142
+ if (!this._streaming.has(sid)) return;
143
+ this._streaming.delete(sid);
144
+ this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
145
+ }
146
+
133
147
  _route(cid, sid, e) {
134
148
  if (e.type === 'queue-operation' || (e.type === 'user' && e.isMeta)) return;
135
149
 
136
150
  if (e.type === 'system') {
137
- if (e.subtype === 'init') { this._bc({ type: 'streaming_start', sessionId: sid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() }); return; }
138
- if (e.subtype === 'turn_duration') { this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() }); return; }
139
- const b = { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens };
140
- this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'system', seq: this._seq(sid), timestamp: Date.now() });
151
+ if (e.subtype === 'init') { this._startStreaming(cid, sid); return; }
152
+ if (e.subtype === 'turn_duration' || e.subtype === 'stop_hook_summary') { this._endStreaming(cid, sid); return; }
153
+ this._emit(cid, sid, { type: 'system', subtype: e.subtype, model: e.model, cwd: e.cwd, tools: e.tools, preTokens: e.compactMetadata?.preTokens }, 'system');
141
154
  return;
142
155
  }
143
156
 
144
157
  if (e.type === 'assistant' && e.message?.content) {
158
+ this._startStreaming(cid, sid);
145
159
  const key = `${sid}:${e.message.id}`;
146
- if (e.message.stop_reason === null || e.message.stop_reason === undefined) { this._frags.set(key, e); return; }
147
- this._frags.delete(key);
148
- for (const b of e.message.content) this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'assistant', seq: this._seq(sid), timestamp: Date.now() });
160
+ const prevCount = this._frags.get(key)?.message?.content?.length || 0;
161
+ const newBlocks = e.message.content.slice(prevCount);
162
+ if (e.message.stop_reason === null || e.message.stop_reason === undefined) this._frags.set(key, e);
163
+ else this._frags.delete(key);
164
+ for (const b of newBlocks) this._emit(cid, sid, b, 'assistant');
149
165
  return;
150
166
  }
151
167
 
152
168
  if (e.type === 'user' && e.message?.content) {
153
- if (e.isCompactSummary) { this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: { type: 'compact_summary', content: e.message.content }, seq: this._seq(sid), timestamp: Date.now() }); return; }
154
- for (const b of e.message.content) if (b.type === 'tool_result') this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'tool_result', seq: this._seq(sid), timestamp: Date.now() });
169
+ if (e.isCompactSummary) { this._emit(cid, sid, { type: 'compact_summary', content: e.message.content }, 'system'); return; }
170
+ this._startStreaming(cid, sid);
171
+ const blocks = Array.isArray(e.message.content) ? e.message.content : [];
172
+ for (const b of blocks) if (b.type === 'tool_result') this._emit(cid, sid, b, 'tool_result');
155
173
  return;
156
174
  }
157
175
 
158
- if (e.type === 'progress') { this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: { type: e.subtype || 'progress', content: e.content }, seq: this._seq(sid), timestamp: Date.now() }); return; }
176
+ if (e.type === 'progress') {
177
+ this._startStreaming(cid, sid);
178
+ const dataType = e.data?.type || e.subtype || 'progress';
179
+ const content = dataType === 'agent_progress' ? e.data?.message : (e.data || e.content);
180
+ this._emit(cid, sid, { type: dataType, agentId: e.agentId, content }, 'progress');
181
+ return;
182
+ }
159
183
 
160
- if (e.isApiErrorMessage && e.error === 'rate_limit') { this._bc({ type: 'streaming_error', sessionId: sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() }); return; }
184
+ if (e.isApiErrorMessage && e.error === 'rate_limit') {
185
+ this._bc({ type: 'streaming_error', sessionId: sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
186
+ return;
187
+ }
161
188
 
162
189
  if (e.type === 'result') {
163
- const b = { type: 'result', result: e.result, subtype: e.subtype, duration_ms: e.duration_ms, total_cost_usd: e.total_cost_usd, is_error: e.is_error || false };
164
- this._bc({ type: 'streaming_progress', sessionId: sid, conversationId: cid, block: b, blockRole: 'result', isResult: true, seq: this._seq(sid), timestamp: Date.now() });
165
- this._bc({ type: 'streaming_complete', sessionId: sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
190
+ this._emit(cid, sid, { type: 'result', result: e.result, subtype: e.subtype, duration_ms: e.duration_ms, total_cost_usd: e.total_cost_usd, is_error: e.is_error || false }, 'result', { isResult: true });
191
+ this._endStreaming(cid, sid);
166
192
  }
167
193
  }
168
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.714",
3
+ "version": "1.0.715",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -910,7 +910,12 @@ class AgentGUIClient {
910
910
  if (chunk.block.type === 'tool_result') {
911
911
  const lastInFrag = bFrag.lastElementChild;
912
912
  if (lastInFrag?.classList?.contains('block-tool-use')) {
913
- this.renderer.mergeResultIntoToolUse(lastInFrag, chunk.block);
913
+ lastInFrag.classList.remove('has-success', 'has-error');
914
+ lastInFrag.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
915
+ const parentIsOpen = lastInFrag.hasAttribute('open');
916
+ const contextWithParent = { ...chunk, parentIsOpen };
917
+ const el = this.renderer.renderBlock(chunk.block, contextWithParent, bFrag);
918
+ if (el) { lastInFrag.appendChild(el); }
914
919
  return;
915
920
  }
916
921
  }
@@ -1582,13 +1587,10 @@ class AgentGUIClient {
1582
1587
  let html = '<div class="message-blocks">';
1583
1588
  if (content.blocks && Array.isArray(content.blocks)) {
1584
1589
  let pendingToolUseClose = false;
1585
- let pendingHasInput = false;
1586
1590
  content.blocks.forEach((block, blockIdx, blocks) => {
1587
1591
  if (block.type !== 'tool_result' && pendingToolUseClose) {
1588
- if (pendingHasInput) html += '</div>';
1589
1592
  html += '</details>';
1590
1593
  pendingToolUseClose = false;
1591
- pendingHasInput = false;
1592
1594
  }
1593
1595
  if (block.type === 'text') {
1594
1596
  const parts = this.parseMarkdownCodeBlocks(block.text);
@@ -1619,11 +1621,10 @@ class AgentGUIClient {
1619
1621
  html += `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(block.language || 'code')} - ${blkLineCount} line${blkLineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(block.code)}</pre></details></div>`;
1620
1622
  }
1621
1623
  } else if (block.type === 'tool_use') {
1622
- let inputContentHtml = '';
1623
- const hasInput = block.input && Object.keys(block.input).length > 0;
1624
- if (hasInput) {
1624
+ let inputHtml = '';
1625
+ if (block.input && Object.keys(block.input).length > 0) {
1625
1626
  const inputStr = JSON.stringify(block.input, null, 2);
1626
- inputContentHtml = `<pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre>`;
1627
+ inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
1627
1628
  }
1628
1629
  const tn = block.name || 'unknown';
1629
1630
  const hasRenderer = typeof StreamingRenderer !== 'undefined';
@@ -1633,40 +1634,27 @@ class AgentGUIClient {
1633
1634
  const typeClass = hasRenderer && this.renderer ? this.renderer._getBlockTypeClass('tool_use') : 'block-type-tool_use';
1634
1635
  const toolColorClass = hasRenderer && this.renderer ? this.renderer._getToolColorClass(tn) : 'tool-color-default';
1635
1636
  const nextBlock = blocks[blockIdx + 1];
1636
- const resultClass = nextBlock?.type === 'tool_result' ? (nextBlock.is_error ? 'has-error tool-result-error' : 'has-success tool-result-success') : '';
1637
- const resultStatusIcon = nextBlock?.type === 'tool_result'
1638
- ? `<span class="folded-tool-status">${nextBlock.is_error
1639
- ? '<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>'
1640
- : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>'
1641
- }</span>` : '';
1642
- if (hasInput) {
1643
- html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><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>` : ''}${resultStatusIcon}</summary><div class="folded-tool-body">${inputContentHtml}`;
1644
- pendingHasInput = true;
1645
- } else {
1646
- html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><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>` : ''}${resultStatusIcon}</summary>`;
1647
- pendingHasInput = false;
1648
- }
1637
+ const resultClass = nextBlock?.type === 'tool_result' ? (nextBlock.is_error ? 'has-error' : 'has-success') : '';
1638
+ html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><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}`;
1649
1639
  pendingToolUseClose = true;
1650
1640
  } else if (block.type === 'tool_result') {
1651
1641
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
1652
1642
  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>`;
1653
- const resultContentHtml = `<div class="folded-tool-result-content">${smartHtml}</div>`;
1643
+ const resultPreview = content.length > 80 ? content.substring(0, 77).replace(/\n/g, ' ') + '...' : content.replace(/\n/g, ' ');
1644
+ const resultIcon = block.is_error
1645
+ ? '<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>'
1646
+ : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1647
+ const resultTypeClass = hasRenderer && this.renderer ? this.renderer._getBlockTypeClass('tool_result') : 'block-type-tool_result';
1648
+ const resultHtml = `<div class="tool-result-inline${block.is_error ? ' tool-result-error' : ' tool-result-success'} ${resultTypeClass}"><div class="tool-result-status"><span class="folded-tool-icon">${resultIcon}</span><span class="folded-tool-name">${block.is_error ? 'Error' : 'Success'}</span></div><div class="folded-tool-body">${smartHtml}</div></div>`;
1654
1649
  if (pendingToolUseClose) {
1655
- if (pendingHasInput) {
1656
- html += resultContentHtml + '</div></details>';
1657
- } else {
1658
- html += `<div class="folded-tool-body">${resultContentHtml}</div></details>`;
1659
- }
1650
+ html += resultHtml + '</details>';
1660
1651
  pendingToolUseClose = false;
1661
1652
  } else {
1662
- html += resultContentHtml;
1653
+ html += resultHtml;
1663
1654
  }
1664
1655
  }
1665
1656
  });
1666
- if (pendingToolUseClose) {
1667
- if (pendingHasInput) html += '</div>';
1668
- html += '</details>';
1669
- }
1657
+ if (pendingToolUseClose) html += '</details>';
1670
1658
  }
1671
1659
  html += '</div>';
1672
1660
  return html;
@@ -2151,8 +2139,12 @@ class AgentGUIClient {
2151
2139
  const lastEl = blocksEl.lastElementChild;
2152
2140
  const toolUseEl = matchById || (lastEl?.classList?.contains('block-tool-use') ? lastEl : null);
2153
2141
  if (toolUseEl) {
2154
- this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
2155
- this.scrollToBottom();
2142
+ toolUseEl.classList.remove('has-success', 'has-error');
2143
+ toolUseEl.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
2144
+ const parentIsOpen = toolUseEl.hasAttribute('open');
2145
+ const contextWithParent = { ...chunk, parentIsOpen };
2146
+ const element = this.renderer.renderBlock(chunk.block, contextWithParent, blocksEl);
2147
+ if (element) { toolUseEl.appendChild(element); this.scrollToBottom(); }
2156
2148
  return;
2157
2149
  }
2158
2150
  }
@@ -2893,7 +2885,14 @@ class AgentGUIClient {
2893
2885
  ? blocksEl.querySelector(`.block-tool-use[data-tool-use-id="${toolUseId}"]`)
2894
2886
  : blocksEl.lastElementChild?.classList?.contains('block-type-tool_use') ? blocksEl.lastElementChild : null;
2895
2887
  if (!toolUseEl) return;
2896
- this.renderer.mergeResultIntoToolUse(toolUseEl, chunk.block);
2888
+ toolUseEl.classList.remove('has-success', 'has-error');
2889
+ toolUseEl.classList.add(chunk.block.is_error ? 'has-error' : 'has-success');
2890
+ const contextWithParent = { ...chunk, parentIsOpen: toolUseEl.hasAttribute('open') };
2891
+ const element = this.renderer.renderBlock(chunk.block, contextWithParent, toolUseEl);
2892
+ if (element) {
2893
+ element.classList.add('block-loaded');
2894
+ toolUseEl.appendChild(element);
2895
+ }
2897
2896
  });
2898
2897
 
2899
2898
  if (isCurrentActiveSession) {
@@ -1261,17 +1261,33 @@ class StreamingRenderer {
1261
1261
  const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
1262
1262
  const isError = (block.is_error || false) && !contentStr.trimStart().startsWith('exec ran successfully.');
1263
1263
 
1264
- const container = document.createElement('div');
1265
- container.className = 'tool-result-pending';
1266
- container.dataset.eventType = 'tool_result';
1267
- container.dataset.isError = isError ? '1' : '0';
1268
- if (block.tool_use_id) container.dataset.toolUseId = block.tool_use_id;
1269
- container.style.display = 'none';
1264
+ const details = document.createElement('details');
1265
+ details.className = 'folded-tool' + (isError ? ' folded-tool-error' : ' folded-tool-success');
1266
+ details.dataset.eventType = 'tool_result';
1267
+ // Only open by default if the content is an image and it's not an error
1268
+ const isImageContent = contentStr.includes('data:image/') || (block.content && block.content.type === 'base64');
1269
+ if (!isError && isImageContent) details.open = true;
1270
+ if (block.tool_use_id) details.dataset.toolUseId = block.tool_use_id;
1271
+ details.classList.add(this._getBlockTypeClass('tool_result'));
1272
+
1273
+ const summary = document.createElement('summary');
1274
+ summary.className = 'folded-tool-bar';
1275
+ const iconSvg = isError
1276
+ ? '<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>'
1277
+ : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1278
+ summary.innerHTML = `
1279
+ <span class="folded-tool-icon">${iconSvg}</span>
1280
+ <span class="folded-tool-name">${isError ? 'Error' : 'Success'}</span>
1281
+ `;
1282
+ details.appendChild(summary);
1270
1283
 
1271
1284
  const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
1272
- container.innerHTML = renderedContent;
1285
+ const body = document.createElement('div');
1286
+ body.className = 'folded-tool-body';
1287
+ body.innerHTML = renderedContent;
1288
+ details.appendChild(body);
1273
1289
 
1274
- return container;
1290
+ return details;
1275
1291
  }
1276
1292
 
1277
1293
  /**
@@ -2143,14 +2159,14 @@ class StreamingRenderer {
2143
2159
  if (!this.outputContainer) return;
2144
2160
 
2145
2161
  const toolUseBlocks = this.outputContainer.querySelectorAll('details.block-tool-use[data-tool-use-id]');
2146
- const toolResultBlocks = this.outputContainer.querySelectorAll('.tool-result-pending[data-tool-use-id]');
2162
+ const toolResultBlocks = this.outputContainer.querySelectorAll('details[data-event-type="tool_result"][data-tool-use-id]');
2147
2163
 
2148
2164
  toolResultBlocks.forEach(resultBlock => {
2149
2165
  const toolUseId = resultBlock.dataset.toolUseId;
2150
- const isError = resultBlock.dataset.isError === '1';
2151
2166
  const toolUseBlock = Array.from(toolUseBlocks).find(b => b.dataset.toolUseId === toolUseId);
2152
2167
 
2153
- if (toolUseBlock) {
2168
+ if (toolUseBlock && toolUseBlock.parentElement === resultBlock.parentElement) {
2169
+ const isError = resultBlock.classList.contains('folded-tool-error');
2154
2170
  const toolUseSummary = toolUseBlock.querySelector(':scope > summary');
2155
2171
 
2156
2172
  if (toolUseSummary && !toolUseSummary.querySelector('.folded-tool-status')) {
@@ -2164,7 +2180,8 @@ class StreamingRenderer {
2164
2180
  toolUseBlock.classList.add(isError ? 'tool-result-error' : 'tool-result-success');
2165
2181
  }
2166
2182
 
2167
- if (resultBlock.innerHTML.trim()) {
2183
+ const resultBody = resultBlock.querySelector('.folded-tool-body');
2184
+ if (resultBody && resultBody.innerHTML.trim()) {
2168
2185
  let toolUseBody = toolUseBlock.querySelector(':scope > .folded-tool-body');
2169
2186
  if (!toolUseBody) {
2170
2187
  toolUseBody = document.createElement('div');
@@ -2173,12 +2190,10 @@ class StreamingRenderer {
2173
2190
  }
2174
2191
  const resultContent = document.createElement('div');
2175
2192
  resultContent.className = 'folded-tool-result-content';
2176
- resultContent.innerHTML = resultBlock.innerHTML;
2193
+ resultContent.innerHTML = resultBody.innerHTML;
2177
2194
  toolUseBody.appendChild(resultContent);
2178
2195
  }
2179
2196
 
2180
- resultBlock.remove();
2181
- } else {
2182
2197
  resultBlock.remove();
2183
2198
  }
2184
2199
  });
@@ -2187,40 +2202,6 @@ class StreamingRenderer {
2187
2202
  /**
2188
2203
  * Auto-scroll to bottom of container
2189
2204
  */
2190
- mergeResultIntoToolUse(toolUseEl, block) {
2191
- const content = block.content || '';
2192
- const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
2193
- const isError = (block.is_error || false) && !contentStr.trimStart().startsWith('exec ran successfully.');
2194
-
2195
- toolUseEl.classList.remove('has-success', 'has-error');
2196
- toolUseEl.classList.add(isError ? 'has-error tool-result-error' : 'has-success tool-result-success');
2197
-
2198
- const summary = toolUseEl.querySelector(':scope > summary');
2199
- if (summary && !summary.querySelector('.folded-tool-status')) {
2200
- const statusSvg = isError
2201
- ? '<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>'
2202
- : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
2203
- const statusSpan = document.createElement('span');
2204
- statusSpan.className = 'folded-tool-status';
2205
- statusSpan.innerHTML = statusSvg;
2206
- summary.appendChild(statusSpan);
2207
- }
2208
-
2209
- const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
2210
- if (renderedContent && renderedContent.trim()) {
2211
- let toolUseBody = toolUseEl.querySelector(':scope > .folded-tool-body');
2212
- if (!toolUseBody) {
2213
- toolUseBody = document.createElement('div');
2214
- toolUseBody.className = 'folded-tool-body';
2215
- toolUseEl.appendChild(toolUseBody);
2216
- }
2217
- const resultContent = document.createElement('div');
2218
- resultContent.className = 'folded-tool-result-content';
2219
- resultContent.innerHTML = renderedContent;
2220
- toolUseBody.appendChild(resultContent);
2221
- }
2222
- }
2223
-
2224
2205
  autoScroll() {
2225
2206
  if (this._scrollRafPending || this._userScrolledUp) return;
2226
2207
  this._scrollRafPending = true;