agentgui 1.0.97 → 1.0.99

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.97",
3
+ "version": "1.0.99",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -475,10 +475,27 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
475
475
  let eventCount = 0;
476
476
 
477
477
  const onEvent = (parsed) => {
478
- if (parsed.type === 'assistant' && parsed.message?.content) {
478
+ eventCount++;
479
+
480
+ if (parsed.type === 'system') {
481
+ broadcastSync({
482
+ type: 'streaming_progress',
483
+ sessionId,
484
+ conversationId,
485
+ block: {
486
+ type: 'system',
487
+ subtype: parsed.subtype,
488
+ model: parsed.model,
489
+ cwd: parsed.cwd,
490
+ tools: parsed.tools,
491
+ session_id: parsed.session_id
492
+ },
493
+ blockIndex: allBlocks.length,
494
+ timestamp: Date.now()
495
+ });
496
+ } else if (parsed.type === 'assistant' && parsed.message?.content) {
479
497
  for (const block of parsed.message.content) {
480
498
  allBlocks.push(block);
481
- eventCount++;
482
499
  broadcastSync({
483
500
  type: 'streaming_progress',
484
501
  sessionId,
@@ -488,16 +505,45 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
488
505
  timestamp: Date.now()
489
506
  });
490
507
  }
491
- } else if (parsed.type === 'result' && parsed.result && allBlocks.length === 0) {
508
+ } else if (parsed.type === 'user' && parsed.message?.content) {
509
+ for (const block of parsed.message.content) {
510
+ if (block.type === 'tool_result') {
511
+ broadcastSync({
512
+ type: 'streaming_progress',
513
+ sessionId,
514
+ conversationId,
515
+ block: {
516
+ type: 'tool_result',
517
+ tool_use_id: block.tool_use_id,
518
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
519
+ is_error: block.is_error || false
520
+ },
521
+ blockIndex: allBlocks.length,
522
+ timestamp: Date.now()
523
+ });
524
+ }
525
+ }
526
+ } else if (parsed.type === 'result') {
492
527
  broadcastSync({
493
528
  type: 'streaming_progress',
494
529
  sessionId,
495
530
  conversationId,
496
- block: { type: 'text', text: parsed.result },
497
- blockIndex: 0,
531
+ block: {
532
+ type: 'result',
533
+ subtype: parsed.subtype,
534
+ duration_ms: parsed.duration_ms,
535
+ total_cost_usd: parsed.total_cost_usd,
536
+ num_turns: parsed.num_turns,
537
+ is_error: parsed.is_error || false,
538
+ result: parsed.result
539
+ },
540
+ blockIndex: allBlocks.length,
498
541
  isResult: true,
499
542
  timestamp: Date.now()
500
543
  });
544
+ if (parsed.result && allBlocks.length === 0) {
545
+ allBlocks.push({ type: 'text', text: String(parsed.result) });
546
+ }
501
547
  }
502
548
  };
503
549
 
package/static/index.html CHANGED
@@ -431,6 +431,170 @@
431
431
  margin: 0.25rem 0;
432
432
  }
433
433
 
434
+ /* --- Streaming block types --- */
435
+ .streaming-block-system {
436
+ padding: 0.5rem 0.75rem;
437
+ margin: 0.25rem 0;
438
+ background: rgba(59,130,246,0.08);
439
+ border: 1px solid rgba(59,130,246,0.2);
440
+ border-radius: 0.375rem;
441
+ font-size: 0.8rem;
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 0.5rem;
445
+ }
446
+
447
+ .system-model-badge {
448
+ background: var(--color-primary);
449
+ color: white;
450
+ padding: 0.125rem 0.5rem;
451
+ border-radius: 1rem;
452
+ font-size: 0.7rem;
453
+ font-weight: 600;
454
+ white-space: nowrap;
455
+ }
456
+
457
+ .system-info {
458
+ color: var(--color-text-secondary);
459
+ font-size: 0.75rem;
460
+ }
461
+
462
+ .streaming-block-tool-use {
463
+ margin: 0.5rem 0;
464
+ border-left: 3px solid #06b6d4;
465
+ background: rgba(6,182,212,0.06);
466
+ border-radius: 0 0.375rem 0.375rem 0;
467
+ overflow: hidden;
468
+ }
469
+
470
+ .tool-use-header {
471
+ padding: 0.5rem 0.75rem;
472
+ font-weight: 600;
473
+ font-size: 0.85rem;
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 0.375rem;
477
+ }
478
+
479
+ .tool-use-icon {
480
+ font-size: 0.9rem;
481
+ opacity: 0.7;
482
+ }
483
+
484
+ .tool-use-name {
485
+ color: #0891b2;
486
+ }
487
+
488
+ html.dark .tool-use-name { color: #22d3ee; }
489
+
490
+ .tool-input-details {
491
+ border-top: 1px solid rgba(6,182,212,0.15);
492
+ }
493
+
494
+ .tool-input-summary {
495
+ padding: 0.375rem 0.75rem;
496
+ font-size: 0.75rem;
497
+ color: var(--color-text-secondary);
498
+ cursor: pointer;
499
+ user-select: none;
500
+ }
501
+
502
+ .tool-input-summary:hover {
503
+ background: rgba(6,182,212,0.08);
504
+ }
505
+
506
+ .tool-input-pre {
507
+ margin: 0;
508
+ padding: 0.5rem 0.75rem;
509
+ font-size: 0.75rem;
510
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
511
+ background: rgba(0,0,0,0.03);
512
+ overflow-x: auto;
513
+ max-height: 200px;
514
+ overflow-y: auto;
515
+ white-space: pre-wrap;
516
+ word-break: break-all;
517
+ }
518
+
519
+ html.dark .tool-input-pre { background: rgba(255,255,255,0.03); }
520
+
521
+ .streaming-block-tool-result {
522
+ margin: 0.25rem 0 0.5rem 0;
523
+ border-radius: 0.375rem;
524
+ background: var(--color-bg-code);
525
+ overflow: hidden;
526
+ }
527
+
528
+ .tool-result-header {
529
+ padding: 0.375rem 0.75rem;
530
+ font-size: 0.7rem;
531
+ border-bottom: 1px solid rgba(255,255,255,0.05);
532
+ }
533
+
534
+ .tool-result-ok-badge {
535
+ color: #10b981;
536
+ font-weight: 600;
537
+ text-transform: uppercase;
538
+ letter-spacing: 0.05em;
539
+ }
540
+
541
+ .tool-result-error-badge {
542
+ color: #ef4444;
543
+ font-weight: 600;
544
+ text-transform: uppercase;
545
+ letter-spacing: 0.05em;
546
+ }
547
+
548
+ .tool-result-pre {
549
+ margin: 0;
550
+ padding: 0.5rem 0.75rem;
551
+ font-size: 0.75rem;
552
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
553
+ color: #d1d5db;
554
+ overflow-x: auto;
555
+ max-height: 300px;
556
+ overflow-y: auto;
557
+ white-space: pre-wrap;
558
+ word-break: break-all;
559
+ }
560
+
561
+ .streaming-block-tool-result.tool-result-error {
562
+ border: 1px solid rgba(239,68,68,0.3);
563
+ }
564
+
565
+ .streaming-block-result {
566
+ padding: 0.5rem 0.75rem;
567
+ margin: 0.5rem 0;
568
+ border-radius: 0.375rem;
569
+ background: rgba(16,185,129,0.08);
570
+ border: 1px solid rgba(16,185,129,0.2);
571
+ font-size: 0.8rem;
572
+ display: flex;
573
+ align-items: center;
574
+ gap: 0.5rem;
575
+ }
576
+
577
+ .streaming-block-result.result-error {
578
+ background: rgba(239,68,68,0.08);
579
+ border-color: rgba(239,68,68,0.2);
580
+ }
581
+
582
+ .result-status {
583
+ font-weight: 600;
584
+ color: #10b981;
585
+ }
586
+
587
+ .result-error .result-status { color: #ef4444; }
588
+
589
+ .result-stats {
590
+ color: var(--color-text-secondary);
591
+ font-size: 0.75rem;
592
+ }
593
+
594
+ .streaming-indicator-label {
595
+ white-space: nowrap;
596
+ }
597
+
434
598
  /* --- Input area: fixed at bottom --- */
435
599
  .input-section {
436
600
  flex-shrink: 0;
@@ -274,7 +274,7 @@ class AgentGUIClient {
274
274
  <div class="message-blocks streaming-blocks"></div>
275
275
  <div class="streaming-indicator" style="display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;">
276
276
  <span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
277
- Thinking...
277
+ <span class="streaming-indicator-label">Thinking...</span>
278
278
  </div>
279
279
  `;
280
280
  messagesEl.appendChild(streamingDiv);
@@ -300,37 +300,75 @@ class AgentGUIClient {
300
300
  if (!blocksEl) return;
301
301
 
302
302
  const indicator = streamingEl.querySelector('.streaming-indicator');
303
+ let indicatorText = 'Responding...';
303
304
 
304
- if (block.type === 'text' && block.text) {
305
+ if (block.type === 'system') {
306
+ const div = document.createElement('div');
307
+ div.className = 'streaming-block-system';
308
+ const toolCount = block.tools ? block.tools.length : 0;
309
+ div.innerHTML = `<span class="system-model-badge">${this.escapeHtml(block.model || 'unknown')}</span> <span class="system-info">${toolCount} tools available</span>`;
310
+ blocksEl.appendChild(div);
311
+ indicatorText = 'Initializing...';
312
+ } else if (block.type === 'text' && block.text) {
305
313
  const existingTextEl = blocksEl.querySelector('.streaming-text-current');
306
314
  if (existingTextEl && !data.isResult) {
307
315
  existingTextEl.innerHTML = this.renderBlockContent(block);
308
316
  } else {
317
+ const prevTextEl = blocksEl.querySelector('.streaming-text-current');
318
+ if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
309
319
  const div = document.createElement('div');
310
320
  div.className = 'message-text streaming-text-current';
311
321
  div.innerHTML = this.renderBlockContent(block);
312
322
  blocksEl.appendChild(div);
313
323
  }
324
+ indicatorText = 'Responding...';
314
325
  } else if (block.type === 'tool_use') {
315
326
  const prevTextEl = blocksEl.querySelector('.streaming-text-current');
316
327
  if (prevTextEl) prevTextEl.classList.remove('streaming-text-current');
317
328
 
318
329
  const div = document.createElement('div');
319
- div.className = 'message-tool';
320
- div.textContent = `[Tool: ${block.name || 'unknown'}]`;
330
+ div.className = 'streaming-block-tool-use';
331
+ div.dataset.toolUseId = block.id || '';
332
+ let inputHtml = '';
333
+ if (block.input && Object.keys(block.input).length > 0) {
334
+ const inputStr = JSON.stringify(block.input, null, 2);
335
+ inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
336
+ }
337
+ div.innerHTML = `<div class="tool-use-header"><span class="tool-use-icon">&#9881;</span> <span class="tool-use-name">${this.escapeHtml(block.name || 'unknown')}</span></div>${inputHtml}`;
321
338
  blocksEl.appendChild(div);
339
+ indicatorText = `Using ${block.name || 'tool'}...`;
322
340
  } else if (block.type === 'tool_result') {
323
341
  const div = document.createElement('div');
324
- div.className = 'message-text';
325
- div.innerHTML = `<em style="color:var(--color-text-secondary)">${this.escapeHtml(String(block.result || '').substring(0, 500))}</em>`;
342
+ div.className = 'streaming-block-tool-result' + (block.is_error ? ' tool-result-error' : '');
343
+ const content = block.content || '';
344
+ const displayContent = content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content;
345
+ div.innerHTML = `<div class="tool-result-header">${block.is_error ? '<span class="tool-result-error-badge">Error</span>' : '<span class="tool-result-ok-badge">Result</span>'}</div><pre class="tool-result-pre">${this.escapeHtml(displayContent)}</pre>`;
326
346
  blocksEl.appendChild(div);
347
+ indicatorText = 'Processing result...';
348
+ } else if (block.type === 'result') {
349
+ const div = document.createElement('div');
350
+ div.className = 'streaming-block-result' + (block.is_error ? ' result-error' : '');
351
+ const duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
352
+ const cost = block.total_cost_usd ? '$' + block.total_cost_usd.toFixed(4) : '';
353
+ const turns = block.num_turns ? block.num_turns + ' turns' : '';
354
+ const parts = [duration, cost, turns].filter(Boolean);
355
+ div.innerHTML = `<span class="result-status">${block.is_error ? 'Failed' : 'Complete'}</span>${parts.length ? ' <span class="result-stats">' + parts.join(' / ') + '</span>' : ''}`;
356
+ blocksEl.appendChild(div);
357
+ indicatorText = 'Complete';
327
358
  }
328
359
 
329
- if (indicator) indicator.querySelector('span:last-child')?.remove();
330
360
  if (indicator) {
331
- const label = document.createElement('span');
332
- label.textContent = block.type === 'tool_use' ? `Using ${block.name}...` : 'Responding...';
333
- indicator.appendChild(label);
361
+ const labelEl = indicator.querySelector('.streaming-indicator-label');
362
+ if (labelEl) {
363
+ labelEl.textContent = indicatorText;
364
+ } else {
365
+ const existingLabel = indicator.querySelector('span:last-child');
366
+ if (existingLabel && !existingLabel.classList.contains('animate-spin')) existingLabel.remove();
367
+ const label = document.createElement('span');
368
+ label.className = 'streaming-indicator-label';
369
+ label.textContent = indicatorText;
370
+ indicator.appendChild(label);
371
+ }
334
372
  }
335
373
 
336
374
  this.scrollToBottom();
@@ -339,10 +377,21 @@ class AgentGUIClient {
339
377
  renderBlockContent(block) {
340
378
  if (block.type === 'text' && block.text) {
341
379
  const text = block.text;
342
- if (text.includes('<') && (text.includes('</') || text.includes('/>'))) {
343
- return text;
380
+ if (this.isHtmlContent(text)) {
381
+ 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>`;
382
+ }
383
+ const parts = this.parseMarkdownCodeBlocks(text);
384
+ if (parts.length === 1 && parts[0].type === 'text') {
385
+ return this.escapeHtml(text);
344
386
  }
345
- return this.escapeHtml(text);
387
+ return parts.map(part => {
388
+ if (part.type === 'html') {
389
+ 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>`;
390
+ } else if (part.type === 'code') {
391
+ return this.renderCodeBlock(part.language, part.code);
392
+ }
393
+ return this.escapeHtml(part.content);
394
+ }).join('');
346
395
  }
347
396
  return this.escapeHtml(JSON.stringify(block));
348
397
  }
@@ -455,10 +504,12 @@ class AgentGUIClient {
455
504
  }
456
505
  }
457
506
 
458
- /**
459
- * Parse markdown code blocks from text
460
- * Returns array of parts with type ('text' or 'code') and content/language/code
461
- */
507
+ isHtmlContent(text) {
508
+ const openTag = /<(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])\b[^>]*>/i;
509
+ const closeTag = /<\/(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])>/i;
510
+ return openTag.test(text) && closeTag.test(text);
511
+ }
512
+
462
513
  parseMarkdownCodeBlocks(text) {
463
514
  const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
464
515
  const parts = [];
@@ -466,14 +517,13 @@ class AgentGUIClient {
466
517
  let match;
467
518
 
468
519
  while ((match = codeBlockRegex.exec(text)) !== null) {
469
- // Add text before the code block
470
520
  if (match.index > lastIndex) {
521
+ const segment = text.substring(lastIndex, match.index);
471
522
  parts.push({
472
- type: 'text',
473
- content: text.substring(lastIndex, match.index)
523
+ type: this.isHtmlContent(segment) ? 'html' : 'text',
524
+ content: segment
474
525
  });
475
526
  }
476
- // Add the code block
477
527
  parts.push({
478
528
  type: 'code',
479
529
  language: match[1] || 'plain',
@@ -482,17 +532,16 @@ class AgentGUIClient {
482
532
  lastIndex = codeBlockRegex.lastIndex;
483
533
  }
484
534
 
485
- // Add remaining text after last code block
486
535
  if (lastIndex < text.length) {
536
+ const segment = text.substring(lastIndex);
487
537
  parts.push({
488
- type: 'text',
489
- content: text.substring(lastIndex)
538
+ type: this.isHtmlContent(segment) ? 'html' : 'text',
539
+ content: segment
490
540
  });
491
541
  }
492
542
 
493
- // If no code blocks found, return the text as-is
494
543
  if (parts.length === 0) {
495
- return [{ type: 'text', content: text }];
544
+ return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
496
545
  }
497
546
 
498
547
  return parts;
@@ -523,16 +572,20 @@ class AgentGUIClient {
523
572
  */
524
573
  renderMessageContent(content) {
525
574
  if (typeof content === 'string') {
575
+ if (this.isHtmlContent(content)) {
576
+ 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>`;
577
+ }
526
578
  return `<div class="message-text">${this.escapeHtml(content)}</div>`;
527
579
  } else if (content && typeof content === 'object' && content.type === 'claude_execution') {
528
580
  let html = '<div class="message-blocks">';
529
581
  if (content.blocks && Array.isArray(content.blocks)) {
530
582
  content.blocks.forEach(block => {
531
583
  if (block.type === 'text') {
532
- // Parse markdown code blocks from text
533
584
  const parts = this.parseMarkdownCodeBlocks(block.text);
534
585
  parts.forEach(part => {
535
- if (part.type === 'text') {
586
+ if (part.type === 'html') {
587
+ 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>`;
588
+ } else if (part.type === 'text') {
536
589
  html += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
537
590
  } else if (part.type === 'code') {
538
591
  html += this.renderCodeBlock(part.language, part.code);
@@ -555,7 +608,16 @@ class AgentGUIClient {
555
608
  html += `<div class="message-code"><pre>${this.escapeHtml(block.code)}</pre></div>`;
556
609
  }
557
610
  } else if (block.type === 'tool_use') {
558
- html += `<div class="message-tool">[Tool: ${this.escapeHtml(block.name)}]</div>`;
611
+ let inputHtml = '';
612
+ if (block.input && Object.keys(block.input).length > 0) {
613
+ const inputStr = JSON.stringify(block.input, null, 2);
614
+ inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
615
+ }
616
+ 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(block.name || 'unknown')}</span></div>${inputHtml}</div>`;
617
+ } else if (block.type === 'tool_result') {
618
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
619
+ const displayContent = content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content;
620
+ html += `<div class="streaming-block-tool-result${block.is_error ? ' tool-result-error' : ''}"><div class="tool-result-header">${block.is_error ? '<span class="tool-result-error-badge">Error</span>' : '<span class="tool-result-ok-badge">Result</span>'}</div><pre class="tool-result-pre">${this.escapeHtml(displayContent)}</pre></div>`;
559
621
  }
560
622
  });
561
623
  }
@@ -809,18 +871,22 @@ class AgentGUIClient {
809
871
  return messages.map(msg => {
810
872
  let contentHtml = '';
811
873
 
812
- // Handle different content types
813
874
  if (typeof msg.content === 'string') {
814
- contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
875
+ if (this.isHtmlContent(msg.content)) {
876
+ 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>`;
877
+ } else {
878
+ contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
879
+ }
815
880
  } else if (msg.content && typeof msg.content === 'object' && msg.content.type === 'claude_execution') {
816
- // Handle Claude execution blocks
817
881
  contentHtml = '<div class="message-blocks">';
818
882
  if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
819
883
  msg.content.blocks.forEach(block => {
820
884
  if (block.type === 'text') {
821
885
  const parts = this.parseMarkdownCodeBlocks(block.text);
822
886
  parts.forEach(part => {
823
- if (part.type === 'text') {
887
+ if (part.type === 'html') {
888
+ 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">${part.content}</div></div>`;
889
+ } else if (part.type === 'text') {
824
890
  contentHtml += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
825
891
  } else if (part.type === 'code') {
826
892
  contentHtml += this.renderCodeBlock(part.language, part.code);
@@ -843,7 +909,16 @@ class AgentGUIClient {
843
909
  contentHtml += `<div class="message-code"><pre>${this.escapeHtml(block.code)}</pre></div>`;
844
910
  }
845
911
  } else if (block.type === 'tool_use') {
846
- contentHtml += `<div class="message-tool">[Tool: ${this.escapeHtml(block.name)}]</div>`;
912
+ let inputHtml = '';
913
+ if (block.input && Object.keys(block.input).length > 0) {
914
+ const inputStr = JSON.stringify(block.input, null, 2);
915
+ inputHtml = `<details class="tool-input-details"><summary class="tool-input-summary">Input</summary><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></details>`;
916
+ }
917
+ 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(block.name || 'unknown')}</span></div>${inputHtml}</div>`;
918
+ } else if (block.type === 'tool_result') {
919
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
920
+ const displayContent = content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content;
921
+ contentHtml += `<div class="streaming-block-tool-result${block.is_error ? ' tool-result-error' : ''}"><div class="tool-result-header">${block.is_error ? '<span class="tool-result-error-badge">Error</span>' : '<span class="tool-result-ok-badge">Result</span>'}</div><pre class="tool-result-pre">${this.escapeHtml(displayContent)}</pre></div>`;
847
922
  }
848
923
  });
849
924
  }
@@ -569,9 +569,12 @@ class StreamingRenderer {
569
569
  return div;
570
570
  }
571
571
 
572
- /**
573
- * Parse markdown code blocks from text
574
- */
572
+ isHtmlContent(text) {
573
+ const openTag = /<(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])\b[^>]*>/i;
574
+ const closeTag = /<\/(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])>/i;
575
+ return openTag.test(text) && closeTag.test(text);
576
+ }
577
+
575
578
  parseMarkdownCodeBlocks(text) {
576
579
  const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
577
580
  const parts = [];
@@ -580,18 +583,20 @@ class StreamingRenderer {
580
583
 
581
584
  while ((match = codeBlockRegex.exec(text)) !== null) {
582
585
  if (match.index > lastIndex) {
583
- parts.push({ type: 'text', content: text.substring(lastIndex, match.index) });
586
+ const segment = text.substring(lastIndex, match.index);
587
+ parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
584
588
  }
585
589
  parts.push({ type: 'code', language: match[1] || 'plain', code: match[2] });
586
590
  lastIndex = codeBlockRegex.lastIndex;
587
591
  }
588
592
 
589
593
  if (lastIndex < text.length) {
590
- parts.push({ type: 'text', content: text.substring(lastIndex) });
594
+ const segment = text.substring(lastIndex);
595
+ parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
591
596
  }
592
597
 
593
598
  if (parts.length === 0) {
594
- return [{ type: 'text', content: text }];
599
+ return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
595
600
  }
596
601
 
597
602
  return parts;
@@ -610,7 +615,9 @@ class StreamingRenderer {
610
615
  const parts = this.parseMarkdownCodeBlocks(text);
611
616
  let html = '';
612
617
  parts.forEach(part => {
613
- if (part.type === 'text') {
618
+ if (part.type === 'html') {
619
+ html += `<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>`;
620
+ } else if (part.type === 'text') {
614
621
  html += `<p class="text-sm leading-relaxed">${this.escapeHtml(part.content)}</p>`;
615
622
  } else if (part.type === 'code') {
616
623
  if (part.language.toLowerCase() === 'html') {