tabminal 3.0.14 → 3.0.16

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/public/app.js CHANGED
@@ -105,6 +105,10 @@ const HEARTBEAT_INTERVAL_MS = 1000;
105
105
  const RECONNECT_RETRY_MS = 5000;
106
106
  const FILE_TREE_REFRESH_INTERVAL_MS = 3000;
107
107
  const FILE_VERSION_CHECK_INTERVAL_MS = 3000;
108
+ const AGENT_TRANSCRIPT_INITIAL_VISIBLE_BLOCKS = 100;
109
+ const AGENT_TRANSCRIPT_WINDOW_STEP = 50;
110
+ const AGENT_TRANSCRIPT_FOLLOW_LATEST_TOLERANCE = 5;
111
+ const WORKSPACE_TAB_TITLE_MAX_LENGTH = 20;
108
112
  const MAIN_SERVER_ID = 'main';
109
113
  const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
110
114
  const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
@@ -417,6 +421,20 @@ function resolveMarkdownLocalTarget(baseFilePath, href) {
417
421
  }
418
422
  }
419
423
 
424
+ function buildMarkdownContextBasePath(filePath = '', baseDirectory = '') {
425
+ const nextFilePath = String(filePath || '').trim();
426
+ if (nextFilePath) {
427
+ return nextFilePath;
428
+ }
429
+ const nextBaseDirectory = String(baseDirectory || '')
430
+ .trim()
431
+ .replace(/\/+$/, '');
432
+ if (!nextBaseDirectory) {
433
+ return '';
434
+ }
435
+ return `${nextBaseDirectory}/__tabminal__.md`;
436
+ }
437
+
420
438
  function slugifyMarkdownHeading(text) {
421
439
  return String(text || '')
422
440
  .trim()
@@ -1535,6 +1553,24 @@ class EditorManager {
1535
1553
  this.agentTranscript = document.createElement('div');
1536
1554
  this.agentTranscript.className = 'agent-panel-transcript';
1537
1555
  this.agentTranscript.addEventListener('click', (event) => {
1556
+ const markdownLink = event.target.closest(
1557
+ 'a[data-markdown-local-path]'
1558
+ );
1559
+ if (markdownLink) {
1560
+ const filePath = String(
1561
+ markdownLink.dataset.markdownLocalPath || ''
1562
+ ).trim();
1563
+ if (!filePath) {
1564
+ return;
1565
+ }
1566
+ event.preventDefault();
1567
+ event.stopPropagation();
1568
+ void this.openLocalMarkdownLink(
1569
+ filePath,
1570
+ String(markdownLink.dataset.markdownLocalHash || '')
1571
+ );
1572
+ return;
1573
+ }
1538
1574
  const anchor = event.target.closest('a');
1539
1575
  if (!anchor) return;
1540
1576
  const href = anchor.getAttribute('href') || '';
@@ -1545,6 +1581,20 @@ class EditorManager {
1545
1581
  void this.openFile(href);
1546
1582
  });
1547
1583
  this.agentTranscript.addEventListener('scroll', () => {
1584
+ const activeAgentTab = getActiveAgentTab();
1585
+ if (
1586
+ activeAgentTab
1587
+ && this.agentTranscript.scrollTop <= 24
1588
+ ) {
1589
+ activeAgentTab.scrollToBottomOnNextRender = false;
1590
+ void this.loadOlderAgentTimeline(activeAgentTab);
1591
+ } else if (
1592
+ activeAgentTab
1593
+ && this.isAgentTranscriptNearBottom(24)
1594
+ ) {
1595
+ activeAgentTab.scrollToBottomOnNextRender = false;
1596
+ void this.loadNewerAgentTimeline(activeAgentTab);
1597
+ }
1548
1598
  this.updateAgentScrollBottomButton();
1549
1599
  this.rememberAgentTranscriptLayout();
1550
1600
  });
@@ -4197,6 +4247,90 @@ class EditorManager {
4197
4247
  }
4198
4248
  }
4199
4249
 
4250
+ getAgentMarkdownBaseDirectory(agentTab, message) {
4251
+ const messageCwd = String(message?.cwd || '').trim();
4252
+ if (messageCwd) {
4253
+ return messageCwd;
4254
+ }
4255
+ const tabCwd = String(agentTab?.cwd || '').trim();
4256
+ if (tabCwd) {
4257
+ return tabCwd;
4258
+ }
4259
+ const session = this.currentSession;
4260
+ return String(session?.cwd || session?.initialCwd || '').trim();
4261
+ }
4262
+
4263
+ async enhanceAgentMarkdownBody(agentTab, message, body) {
4264
+ if (!(body instanceof HTMLElement) || !message?.text) {
4265
+ return;
4266
+ }
4267
+ if (agentTab?.busy) {
4268
+ return;
4269
+ }
4270
+ if (isAgentMessageStreaming(agentTab, message)) {
4271
+ return;
4272
+ }
4273
+ const session = this.currentSession;
4274
+ if (!session) {
4275
+ return;
4276
+ }
4277
+
4278
+ const sourceText = String(message.text || '');
4279
+ const cachedMarkdown = getAgentMessageMarkdownCache(message);
4280
+ if (cachedMarkdown) {
4281
+ body.classList.remove('plain');
4282
+ body.classList.add('markdown');
4283
+ body.innerHTML = cachedMarkdown;
4284
+ return;
4285
+ }
4286
+
4287
+ const renderToken = `${Date.now()}:${Math.random()}`;
4288
+ body.dataset.markdownRenderToken = renderToken;
4289
+
4290
+ try {
4291
+ const { renderer } = await loadMarkdownPreviewBundle();
4292
+ if (
4293
+ String(message.text || '') !== sourceText
4294
+ || isAgentMessageStreaming(agentTab, message)
4295
+ ) {
4296
+ return;
4297
+ }
4298
+ const rendered = renderer.render(sourceText);
4299
+ const sanitized = DOMPurify.sanitize(rendered, {
4300
+ USE_PROFILES: {
4301
+ html: true,
4302
+ mathMl: true,
4303
+ svg: true
4304
+ }
4305
+ });
4306
+ const template = document.createElement('template');
4307
+ template.innerHTML = sanitized;
4308
+ const basePath = buildMarkdownContextBasePath(
4309
+ '',
4310
+ this.getAgentMarkdownBaseDirectory(agentTab, message)
4311
+ );
4312
+ this.decorateMarkdownPreviewContent(
4313
+ template.content,
4314
+ basePath,
4315
+ session
4316
+ );
4317
+ const nextMarkdownHtml = template.innerHTML;
4318
+ message.markdownRenderSource = sourceText;
4319
+ message.markdownRenderHtml = nextMarkdownHtml;
4320
+ if (
4321
+ !body.isConnected
4322
+ || body.dataset.markdownRenderToken !== renderToken
4323
+ ) {
4324
+ return;
4325
+ }
4326
+ body.classList.remove('plain');
4327
+ body.classList.add('markdown');
4328
+ body.replaceChildren(template.content);
4329
+ } catch {
4330
+ // Keep the lightweight fallback rendering.
4331
+ }
4332
+ }
4333
+
4200
4334
  scrollMarkdownPreviewHash(hash) {
4201
4335
  const nextHash = String(hash || '').trim();
4202
4336
  if (!nextHash || !this.markdownPreviewScroll) {
@@ -5171,7 +5305,10 @@ class EditorManager {
5171
5305
  );
5172
5306
 
5173
5307
  const label = document.createElement('span');
5174
- label.textContent = getAgentDisplayLabel(agentTab);
5308
+ tab.title = String(getAgentDisplayLabel(agentTab) || '').trim();
5309
+ label.textContent = formatWorkspaceTabTitle(
5310
+ getAgentDisplayLabel(agentTab)
5311
+ );
5175
5312
 
5176
5313
  const closeBtn = document.createElement('span');
5177
5314
  closeBtn.className = 'close-btn';
@@ -5475,7 +5612,9 @@ class EditorManager {
5475
5612
  this.hideMarkdownPreview();
5476
5613
  this.emptyState.style.display = 'none';
5477
5614
  this.agentContainer.style.display = 'flex';
5478
- this.renderAgentPanel(agentTab);
5615
+ this.renderAgentPanel(agentTab, {
5616
+ reason: isRestore ? 'activate-restore' : 'activate'
5617
+ });
5479
5618
  }
5480
5619
 
5481
5620
  async closeAgentTab(agentTabKey) {
@@ -5485,8 +5624,7 @@ class EditorManager {
5485
5624
  removeAgentTab(agentTabKey);
5486
5625
  }
5487
5626
 
5488
- renderAgentPanel(agentTab) {
5489
- this.disposeAgentEmbeddedEditors();
5627
+ renderAgentPanel(agentTab, options = {}) {
5490
5628
  const previousLayout = this.captureAgentTranscriptLayout();
5491
5629
  const previousScrollTop = previousLayout?.scrollTop || 0;
5492
5630
  const wasNearBottom = this.isAgentTranscriptLayoutNearBottom(
@@ -5550,50 +5688,97 @@ class EditorManager {
5550
5688
 
5551
5689
  this.renderAgentComposerAttachments(agentTab);
5552
5690
 
5553
- this.agentTranscript.innerHTML = '';
5554
5691
  const timeline = getAgentTimelineItems(agentTab);
5692
+ const shouldPinToBottom = wasNearBottom || (
5693
+ agentTab.scrollToBottomOnNextRender
5694
+ && isAgentTranscriptWindowNearLatest(
5695
+ agentTab,
5696
+ timeline.length
5697
+ )
5698
+ );
5699
+ const transcriptWindow = getAgentTranscriptWindow(
5700
+ agentTab,
5701
+ timeline.length,
5702
+ { pinToBottom: shouldPinToBottom }
5703
+ );
5704
+ const visibleTimeline = timeline.slice(
5705
+ transcriptWindow.start,
5706
+ transcriptWindow.end
5707
+ );
5555
5708
  if (timeline.length === 0) {
5556
- this.agentTranscript.appendChild(
5557
- this.buildAgentEmptyState(agentTab)
5558
- );
5709
+ const existingChildren = Array.from(this.agentTranscript.children);
5710
+ const emptyState = this.buildAgentEmptyState(agentTab);
5711
+ this.agentTranscript.replaceChildren(emptyState);
5712
+ for (const node of existingChildren) {
5713
+ this.disposeAgentTimelineNode(node);
5714
+ }
5559
5715
  } else {
5560
- for (const [index, entry] of timeline.entries()) {
5561
- let node = null;
5562
- if (entry.type === 'message') {
5563
- node = this.buildAgentMessageNode(agentTab, entry.value);
5564
- } else if (entry.type === 'tool') {
5565
- node = this.buildAgentToolNode(agentTab, entry.value);
5566
- } else if (entry.type === 'permission') {
5567
- node = this.buildAgentPermissionNode(
5568
- agentTab,
5569
- entry.value
5570
- );
5571
- } else if (entry.type === 'plan') {
5572
- node = this.buildAgentPlanHistoryNode(
5573
- agentTab,
5574
- entry.value
5575
- );
5716
+ const existingChildren = Array.from(this.agentTranscript.children);
5717
+ const existingByKey = new Map();
5718
+ for (const node of existingChildren) {
5719
+ const key = node?.dataset?.timelineKey || '';
5720
+ if (key) {
5721
+ existingByKey.set(key, node);
5576
5722
  }
5723
+ }
5724
+ const nextNodes = [];
5725
+ for (const [index, entry] of visibleTimeline.entries()) {
5726
+ const timelineIndex = transcriptWindow.start + index;
5727
+ const timelineKey = getAgentTimelineItemKey(
5728
+ entry,
5729
+ timelineIndex
5730
+ );
5731
+ const renderSignature = getAgentTimelineRenderSignature(
5732
+ agentTab,
5733
+ entry
5734
+ );
5735
+ const turnStart = timelineIndex > 0
5736
+ && entry.type === 'message'
5737
+ && String(entry.value?.role || '').toLowerCase()
5738
+ === 'user';
5739
+ let node = existingByKey.get(timelineKey) || null;
5577
5740
  if (node) {
5741
+ existingByKey.delete(timelineKey);
5578
5742
  if (
5579
- index > 0
5580
- && entry.type === 'message'
5581
- && String(entry.value?.role || '').toLowerCase()
5582
- === 'user'
5743
+ node.dataset.renderSignature !== renderSignature
5583
5744
  ) {
5584
- node.classList.add('agent-turn-start');
5745
+ this.disposeAgentTimelineNode(node);
5746
+ node = null;
5585
5747
  }
5586
- this.agentTranscript.appendChild(node);
5587
5748
  }
5749
+ if (!node) {
5750
+ node = this.buildAgentTimelineNode(
5751
+ agentTab,
5752
+ entry,
5753
+ timelineIndex
5754
+ );
5755
+ }
5756
+ if (node) {
5757
+ node.dataset.timelineKey = timelineKey;
5758
+ node.dataset.renderSignature = renderSignature;
5759
+ node.classList.toggle('agent-turn-start', turnStart);
5760
+ nextNodes.push(node);
5761
+ }
5762
+ }
5763
+ for (const node of existingByKey.values()) {
5764
+ this.disposeAgentTimelineNode(node);
5588
5765
  }
5766
+ this.applyAgentTranscriptNodeList(nextNodes);
5589
5767
  }
5590
- const shouldPinToBottom = agentTab.scrollToBottomOnNextRender
5591
- || wasNearBottom;
5592
- if (shouldPinToBottom) {
5768
+ this.rebuildAgentEmbeddedTerminalRegistry();
5769
+ if (options.preserveTranscriptAnchor) {
5770
+ const restored = this.restoreAgentTranscriptAnchor(
5771
+ options.preserveTranscriptAnchor
5772
+ );
5773
+ if (!restored) {
5774
+ this.agentTranscript.scrollTop = previousScrollTop;
5775
+ }
5776
+ } else if (shouldPinToBottom) {
5593
5777
  this.agentTranscript.scrollTop = this.agentTranscript.scrollHeight;
5594
5778
  agentTab.scrollToBottomOnNextRender = false;
5595
5779
  } else {
5596
5780
  this.agentTranscript.scrollTop = previousScrollTop;
5781
+ agentTab.scrollToBottomOnNextRender = false;
5597
5782
  }
5598
5783
  this.updateAgentScrollBottomButton();
5599
5784
  this.rememberAgentTranscriptLayout();
@@ -5611,6 +5796,149 @@ class EditorManager {
5611
5796
  this.scheduleAgentTranscriptViewportUpdate(shouldPinToBottom);
5612
5797
  }
5613
5798
 
5799
+ buildAgentTimelineNode(agentTab, entry, timelineIndex) {
5800
+ if (!entry) {
5801
+ return null;
5802
+ }
5803
+ let node = null;
5804
+ if (entry.type === 'message') {
5805
+ node = this.buildAgentMessageNode(agentTab, entry.value);
5806
+ } else if (entry.type === 'tool') {
5807
+ node = this.buildAgentToolNode(agentTab, entry.value);
5808
+ } else if (entry.type === 'permission') {
5809
+ node = this.buildAgentPermissionNode(
5810
+ agentTab,
5811
+ entry.value
5812
+ );
5813
+ } else if (entry.type === 'plan') {
5814
+ node = this.buildAgentPlanHistoryNode(
5815
+ agentTab,
5816
+ entry.value
5817
+ );
5818
+ }
5819
+ if (!node) {
5820
+ return null;
5821
+ }
5822
+ node.dataset.timelineKey = getAgentTimelineItemKey(
5823
+ entry,
5824
+ timelineIndex
5825
+ );
5826
+ node.dataset.renderSignature = getAgentTimelineRenderSignature(
5827
+ agentTab,
5828
+ entry
5829
+ );
5830
+ return node;
5831
+ }
5832
+
5833
+ applyAgentTranscriptNodeList(nextNodes) {
5834
+ if (!this.agentTranscript) {
5835
+ return;
5836
+ }
5837
+ const keepNodes = new Set(nextNodes);
5838
+ let cursor = this.agentTranscript.firstChild;
5839
+ for (const node of nextNodes) {
5840
+ if (node === cursor) {
5841
+ cursor = cursor?.nextSibling || null;
5842
+ continue;
5843
+ }
5844
+ this.agentTranscript.insertBefore(node, cursor);
5845
+ }
5846
+ for (const node of Array.from(this.agentTranscript.children)) {
5847
+ if (!keepNodes.has(node)) {
5848
+ node.remove();
5849
+ }
5850
+ }
5851
+ }
5852
+
5853
+ async loadOlderAgentTimeline(agentTab) {
5854
+ if (!agentTab || agentTab.historyWindowLoading) {
5855
+ return;
5856
+ }
5857
+ const timeline = getAgentTimelineItems(agentTab);
5858
+ const transcriptWindow = getAgentTranscriptWindow(
5859
+ agentTab,
5860
+ timeline.length
5861
+ );
5862
+ if (transcriptWindow.start <= 0) {
5863
+ return;
5864
+ }
5865
+ agentTab.scrollToBottomOnNextRender = false;
5866
+ const currentWindowSize = transcriptWindow.end
5867
+ - transcriptWindow.start;
5868
+ const step = Math.min(
5869
+ AGENT_TRANSCRIPT_WINDOW_STEP,
5870
+ transcriptWindow.start
5871
+ );
5872
+ const nextStart = Math.max(0, transcriptWindow.start - step);
5873
+ const anchor = this.captureAgentTranscriptAnchor(
5874
+ getAgentTimelineItemKey(
5875
+ timeline[transcriptWindow.start],
5876
+ transcriptWindow.start
5877
+ )
5878
+ );
5879
+ agentTab.historyWindowLoading = true;
5880
+ agentTab.historyWindowStart = nextStart;
5881
+ agentTab.historyWindowEnd = Math.min(
5882
+ timeline.length,
5883
+ nextStart + currentWindowSize
5884
+ );
5885
+ try {
5886
+ this.renderAgentPanel(agentTab, {
5887
+ reason: 'history-older',
5888
+ preserveTranscriptAnchor: anchor
5889
+ });
5890
+ } finally {
5891
+ agentTab.historyWindowLoading = false;
5892
+ }
5893
+ }
5894
+
5895
+ async loadNewerAgentTimeline(agentTab) {
5896
+ if (!agentTab || agentTab.historyWindowLoading) {
5897
+ return;
5898
+ }
5899
+ const timeline = getAgentTimelineItems(agentTab);
5900
+ const transcriptWindow = getAgentTranscriptWindow(
5901
+ agentTab,
5902
+ timeline.length
5903
+ );
5904
+ if (transcriptWindow.end >= timeline.length) {
5905
+ return;
5906
+ }
5907
+ agentTab.scrollToBottomOnNextRender = false;
5908
+ const currentWindowSize = transcriptWindow.end
5909
+ - transcriptWindow.start;
5910
+ const step = Math.min(
5911
+ AGENT_TRANSCRIPT_WINDOW_STEP,
5912
+ timeline.length - transcriptWindow.end
5913
+ );
5914
+ const nextEnd = Math.min(
5915
+ timeline.length,
5916
+ transcriptWindow.end + step
5917
+ );
5918
+ const nextStart = Math.max(0, nextEnd - currentWindowSize);
5919
+ const anchorIndex = Math.max(
5920
+ transcriptWindow.start,
5921
+ transcriptWindow.end - 1
5922
+ );
5923
+ const anchor = this.captureAgentTranscriptAnchor(
5924
+ getAgentTimelineItemKey(
5925
+ timeline[anchorIndex],
5926
+ anchorIndex
5927
+ )
5928
+ );
5929
+ agentTab.historyWindowLoading = true;
5930
+ agentTab.historyWindowStart = nextStart;
5931
+ agentTab.historyWindowEnd = nextEnd;
5932
+ try {
5933
+ this.renderAgentPanel(agentTab, {
5934
+ reason: 'history-newer',
5935
+ preserveTranscriptAnchor: anchor
5936
+ });
5937
+ } finally {
5938
+ agentTab.historyWindowLoading = false;
5939
+ }
5940
+ }
5941
+
5614
5942
  refreshAgentTimelineTimestamps() {
5615
5943
  if (!this.agentContainer || this.agentContainer.style.display === 'none') {
5616
5944
  return;
@@ -5912,8 +6240,23 @@ class EditorManager {
5912
6240
  message.role === 'assistant'
5913
6241
  && message.kind === 'message'
5914
6242
  ) {
5915
- body.classList.add('markdown');
5916
- body.innerHTML = renderAgentMessageMarkdown(message.text || '');
6243
+ const cachedMarkdown = getAgentMessageMarkdownCache(message);
6244
+ if (
6245
+ !agentTab?.busy
6246
+ && !isAgentMessageStreaming(agentTab, message)
6247
+ && cachedMarkdown
6248
+ ) {
6249
+ body.classList.add('markdown');
6250
+ body.innerHTML = cachedMarkdown;
6251
+ } else {
6252
+ body.classList.add('plain');
6253
+ body.textContent = message.text || '';
6254
+ void this.enhanceAgentMarkdownBody(
6255
+ agentTab,
6256
+ message,
6257
+ body
6258
+ );
6259
+ }
5917
6260
  } else {
5918
6261
  body.classList.add('plain');
5919
6262
  body.textContent = message.text || '';
@@ -5999,9 +6342,27 @@ class EditorManager {
5999
6342
  toolStatusClass
6000
6343
  );
6001
6344
  details.appendChild(summary);
6002
- details.appendChild(
6003
- this.buildAgentSectionBody(details, section)
6004
- );
6345
+ const bodyHost = document.createElement('div');
6346
+ bodyHost.className = 'agent-tool-call-section-content';
6347
+ const mountBody = () => {
6348
+ if (bodyHost.dataset.mounted === 'true') {
6349
+ return;
6350
+ }
6351
+ bodyHost.dataset.mounted = 'true';
6352
+ bodyHost.appendChild(
6353
+ this.buildAgentSectionBody(details, section)
6354
+ );
6355
+ };
6356
+ details.appendChild(bodyHost);
6357
+ if (details.open) {
6358
+ queueMicrotask(mountBody);
6359
+ } else {
6360
+ details.addEventListener('toggle', () => {
6361
+ if (details.open) {
6362
+ mountBody();
6363
+ }
6364
+ }, { once: true });
6365
+ }
6005
6366
  sectionContainer.appendChild(details);
6006
6367
  }
6007
6368
  node.appendChild(sectionContainer);
@@ -6093,9 +6454,27 @@ class EditorManager {
6093
6454
  permission.status || 'pending'
6094
6455
  );
6095
6456
  details.appendChild(summary);
6096
- details.appendChild(
6097
- this.buildAgentSectionBody(details, section)
6098
- );
6457
+ const bodyHost = document.createElement('div');
6458
+ bodyHost.className = 'agent-tool-call-section-content';
6459
+ const mountBody = () => {
6460
+ if (bodyHost.dataset.mounted === 'true') {
6461
+ return;
6462
+ }
6463
+ bodyHost.dataset.mounted = 'true';
6464
+ bodyHost.appendChild(
6465
+ this.buildAgentSectionBody(details, section)
6466
+ );
6467
+ };
6468
+ details.appendChild(bodyHost);
6469
+ if (details.open) {
6470
+ queueMicrotask(mountBody);
6471
+ } else {
6472
+ details.addEventListener('toggle', () => {
6473
+ if (details.open) {
6474
+ mountBody();
6475
+ }
6476
+ }, { once: true });
6477
+ }
6099
6478
  sectionContainer.appendChild(details);
6100
6479
  }
6101
6480
  card.appendChild(sectionContainer);
@@ -6158,16 +6537,72 @@ class EditorManager {
6158
6537
 
6159
6538
  disposeAgentEmbeddedEditors() {
6160
6539
  this.agentEmbeddedTerminals.clear();
6161
- for (const disposable of this.agentEmbeddedEditors) {
6162
- try {
6163
- disposable.dispose();
6164
- } catch {
6165
- // Ignore embedded editor disposal failures.
6166
- }
6540
+ if (!this.agentTranscript) {
6541
+ this.agentEmbeddedEditors = [];
6542
+ return;
6543
+ }
6544
+ for (const node of Array.from(this.agentTranscript.children)) {
6545
+ this.disposeAgentTimelineNode(node);
6167
6546
  }
6168
6547
  this.agentEmbeddedEditors = [];
6169
6548
  }
6170
6549
 
6550
+ trackAgentTimelineDisposable(node, disposable) {
6551
+ if (!node || !disposable) {
6552
+ return;
6553
+ }
6554
+ if (!Array.isArray(node.__agentDisposables)) {
6555
+ node.__agentDisposables = [];
6556
+ }
6557
+ node.__agentDisposables.push(disposable);
6558
+ }
6559
+
6560
+ disposeAgentTimelineNode(node) {
6561
+ if (!node || typeof node.querySelectorAll !== 'function') {
6562
+ return;
6563
+ }
6564
+ const ownedNodes = [
6565
+ node,
6566
+ ...Array.from(node.querySelectorAll('*'))
6567
+ ];
6568
+ for (const ownedNode of ownedNodes) {
6569
+ const disposables = Array.isArray(ownedNode.__agentDisposables)
6570
+ ? ownedNode.__agentDisposables
6571
+ : [];
6572
+ for (const disposable of disposables) {
6573
+ try {
6574
+ disposable.dispose();
6575
+ } catch {
6576
+ // Ignore embedded editor disposal failures.
6577
+ }
6578
+ }
6579
+ ownedNode.__agentDisposables = [];
6580
+ if (ownedNode.__agentTerminalBinding) {
6581
+ ownedNode.__agentTerminalBinding = null;
6582
+ }
6583
+ }
6584
+ }
6585
+
6586
+ rebuildAgentEmbeddedTerminalRegistry() {
6587
+ this.agentEmbeddedTerminals.clear();
6588
+ if (!this.agentTranscript) {
6589
+ return;
6590
+ }
6591
+ const terminalHosts = this.agentTranscript.querySelectorAll(
6592
+ '.agent-tool-call-terminal-host'
6593
+ );
6594
+ for (const host of terminalHosts) {
6595
+ const binding = host.__agentTerminalBinding;
6596
+ if (!binding?.terminalId) {
6597
+ continue;
6598
+ }
6599
+ if (!this.agentEmbeddedTerminals.has(binding.terminalId)) {
6600
+ this.agentEmbeddedTerminals.set(binding.terminalId, []);
6601
+ }
6602
+ this.agentEmbeddedTerminals.get(binding.terminalId).push(binding);
6603
+ }
6604
+ }
6605
+
6171
6606
  buildAgentSectionBody(details, section) {
6172
6607
  if (
6173
6608
  section?.kind === 'diff'
@@ -6231,7 +6666,8 @@ class EditorManager {
6231
6666
  + '"JetBrains Mono", Menlo, Consolas, monospace'
6232
6667
  }
6233
6668
  );
6234
- this.agentEmbeddedEditors.push(editor, model);
6669
+ this.trackAgentTimelineDisposable(host, editor);
6670
+ this.trackAgentTimelineDisposable(host, model);
6235
6671
  details.addEventListener('toggle', () => {
6236
6672
  if (details.open) {
6237
6673
  requestAnimationFrame(() => {
@@ -6318,20 +6754,19 @@ class EditorManager {
6318
6754
  }
6319
6755
  });
6320
6756
  if (terminalId) {
6321
- if (!this.agentEmbeddedTerminals.has(terminalId)) {
6322
- this.agentEmbeddedTerminals.set(terminalId, []);
6323
- }
6324
- this.agentEmbeddedTerminals.get(terminalId).push({
6325
- meta,
6326
- header,
6327
- openButton,
6328
- terminalNode,
6329
- terminal: embeddedTerm,
6330
- fitAddon,
6757
+ host.__agentTerminalBinding = {
6758
+ terminalId,
6759
+ meta,
6760
+ header,
6761
+ openButton,
6762
+ terminalNode,
6763
+ terminal: embeddedTerm,
6764
+ fitAddon,
6331
6765
  layout: layoutTerminal
6332
- });
6766
+ };
6333
6767
  }
6334
- this.agentEmbeddedEditors.push(embeddedTerm, fitAddon);
6768
+ this.trackAgentTimelineDisposable(host, embeddedTerm);
6769
+ this.trackAgentTimelineDisposable(host, fitAddon);
6335
6770
 
6336
6771
  return host;
6337
6772
  }
@@ -6445,11 +6880,9 @@ class EditorManager {
6445
6880
  original: originalModel,
6446
6881
  modified: modifiedModel
6447
6882
  });
6448
- this.agentEmbeddedEditors.push(
6449
- diffEditor,
6450
- originalModel,
6451
- modifiedModel
6452
- );
6883
+ this.trackAgentTimelineDisposable(host, diffEditor);
6884
+ this.trackAgentTimelineDisposable(host, originalModel);
6885
+ this.trackAgentTimelineDisposable(host, modifiedModel);
6453
6886
  details.addEventListener('toggle', () => {
6454
6887
  if (details.open) {
6455
6888
  requestAnimationFrame(() => {
@@ -6790,6 +7223,51 @@ class EditorManager {
6790
7223
  };
6791
7224
  }
6792
7225
 
7226
+ findAgentTranscriptNodeByKey(timelineKey = '') {
7227
+ if (!this.agentTranscript || !timelineKey) {
7228
+ return null;
7229
+ }
7230
+ for (const node of this.agentTranscript.children) {
7231
+ if (node?.dataset?.timelineKey === timelineKey) {
7232
+ return node;
7233
+ }
7234
+ }
7235
+ return null;
7236
+ }
7237
+
7238
+ captureAgentTranscriptAnchor(timelineKey = '') {
7239
+ const node = this.findAgentTranscriptNodeByKey(timelineKey);
7240
+ if (!node || !this.agentTranscript) {
7241
+ return null;
7242
+ }
7243
+ return {
7244
+ timelineKey,
7245
+ scrollTop: this.agentTranscript.scrollTop,
7246
+ offsetTop: node.offsetTop
7247
+ };
7248
+ }
7249
+
7250
+ restoreAgentTranscriptAnchor(anchor = null) {
7251
+ if (!anchor || !this.agentTranscript) {
7252
+ return false;
7253
+ }
7254
+ const node = this.findAgentTranscriptNodeByKey(
7255
+ anchor.timelineKey || ''
7256
+ );
7257
+ if (!node) {
7258
+ return false;
7259
+ }
7260
+ const previousOffsetTop = Number.isFinite(anchor.offsetTop)
7261
+ ? anchor.offsetTop
7262
+ : 0;
7263
+ const previousScrollTop = Number.isFinite(anchor.scrollTop)
7264
+ ? anchor.scrollTop
7265
+ : 0;
7266
+ this.agentTranscript.scrollTop = previousScrollTop
7267
+ + (node.offsetTop - previousOffsetTop);
7268
+ return true;
7269
+ }
7270
+
6793
7271
  rememberAgentTranscriptLayout() {
6794
7272
  this.agentTranscriptLayout = this.captureAgentTranscriptLayout();
6795
7273
  }
@@ -6810,6 +7288,32 @@ class EditorManager {
6810
7288
  }
6811
7289
 
6812
7290
  scrollAgentTranscriptToBottom() {
7291
+ const activeTab = getActiveAgentTab();
7292
+ if (activeTab) {
7293
+ const total = getAgentTimelineItems(activeTab).length;
7294
+ const transcriptWindow = getAgentTranscriptWindow(
7295
+ activeTab,
7296
+ total,
7297
+ { pinToBottom: false }
7298
+ );
7299
+ const latestWindow = getAgentTranscriptWindow(
7300
+ null,
7301
+ total,
7302
+ { pinToBottom: true }
7303
+ );
7304
+ const alreadyLatest = transcriptWindow.start === latestWindow.start
7305
+ && transcriptWindow.end === latestWindow.end;
7306
+ if (!alreadyLatest) {
7307
+ activeTab.historyWindowStart = latestWindow.start;
7308
+ activeTab.historyWindowEnd = latestWindow.end;
7309
+ activeTab.scrollToBottomOnNextRender = true;
7310
+ this.renderAgentPanel(activeTab, {
7311
+ reason: 'scroll-latest'
7312
+ });
7313
+ return;
7314
+ }
7315
+ activeTab.scrollToBottomOnNextRender = false;
7316
+ }
6813
7317
  if (!this.agentTranscript) return;
6814
7318
  this.agentTranscript.scrollTop = this.agentTranscript.scrollHeight;
6815
7319
  this.updateAgentScrollBottomButton();
@@ -8315,10 +8819,11 @@ class Session {
8315
8819
 
8316
8820
  const titleEl = tab.querySelector('.title');
8317
8821
  const titleTextEl = tab.querySelector('.tab-title-text');
8822
+ const displayTitle = formatWorkspaceTabTitle(this.title);
8318
8823
  if (titleTextEl) {
8319
- titleTextEl.textContent = this.title;
8824
+ titleTextEl.textContent = displayTitle;
8320
8825
  } else if (titleEl) {
8321
- titleEl.textContent = this.title;
8826
+ titleEl.textContent = displayTitle;
8322
8827
  }
8323
8828
 
8324
8829
  const titleIconEl = tab.querySelector('.tab-status-icon');
@@ -8747,6 +9252,10 @@ class AgentTab {
8747
9252
  this.scrollToBottomOnNextRender = true;
8748
9253
  this.busySyncTimer = null;
8749
9254
  this.planHistory = [];
9255
+ this.historyWindowStart = -1;
9256
+ this.historyWindowEnd = -1;
9257
+ this.historyWindowLoading = false;
9258
+ this.streamingAssistantStreamKey = '';
8750
9259
  this.resumeSessions = [];
8751
9260
  this.resumeSessionsLoadedAt = 0;
8752
9261
  this.resumeSessionsPromise = null;
@@ -8954,9 +9463,23 @@ class AgentTab {
8954
9463
  break;
8955
9464
  case 'message_open':
8956
9465
  this.#upsertMessage(message.message);
9466
+ if (
9467
+ message.message?.role === 'assistant'
9468
+ && message.message?.kind === 'message'
9469
+ && typeof message.message?.streamKey === 'string'
9470
+ ) {
9471
+ this.streamingAssistantStreamKey = message.message.streamKey;
9472
+ }
8957
9473
  break;
8958
9474
  case 'message_chunk':
8959
9475
  this.#appendChunk(message);
9476
+ if (
9477
+ message.role === 'assistant'
9478
+ && message.kind === 'message'
9479
+ && typeof message.streamKey === 'string'
9480
+ ) {
9481
+ this.streamingAssistantStreamKey = message.streamKey;
9482
+ }
8960
9483
  break;
8961
9484
  case 'session_update':
8962
9485
  this.#applySessionUpdate(message.update || {});
@@ -9052,6 +9575,9 @@ class AgentTab {
9052
9575
  default:
9053
9576
  break;
9054
9577
  }
9578
+ if (!this.busy) {
9579
+ this.streamingAssistantStreamKey = '';
9580
+ }
9055
9581
  const shouldAutostartQueuedPrompt = (
9056
9582
  wasBusy
9057
9583
  && !this.busy
@@ -9305,7 +9831,7 @@ class AgentTab {
9305
9831
  const byId = this.messages.findIndex(
9306
9832
  (message) => message.id === candidate.id
9307
9833
  );
9308
- if (byId !== -1) return byId;
9834
+ return byId;
9309
9835
  }
9310
9836
  if (!candidate.streamKey) return -1;
9311
9837
  for (let index = this.messages.length - 1; index >= 0; index -= 1) {
@@ -9325,39 +9851,51 @@ class AgentTab {
9325
9851
  if (!message) return;
9326
9852
  const index = this.#findMessageIndex(message);
9327
9853
  if (index === -1) {
9328
- this.messages.push(this.#normalizeMessage(message));
9854
+ const nextMessage = this.#normalizeMessage(message);
9855
+ clearAgentMessageMarkdownCache(nextMessage);
9856
+ this.messages.push(nextMessage);
9329
9857
  return;
9330
9858
  }
9331
9859
 
9332
9860
  const previous = this.messages[index];
9333
9861
  const nextMessage = this.#normalizeMessage(message, previous.order);
9862
+ const mergedText = selectAgentMessageText(previous.text, nextMessage.text);
9334
9863
  this.messages[index] = {
9335
9864
  ...previous,
9336
9865
  ...nextMessage,
9337
9866
  createdAt: nextMessage.createdAt || previous.createdAt || '',
9338
- text: selectAgentMessageText(previous.text, nextMessage.text)
9867
+ text: mergedText
9339
9868
  };
9869
+ if (mergedText !== (previous.text || '')) {
9870
+ clearAgentMessageMarkdownCache(this.messages[index]);
9871
+ }
9340
9872
  }
9341
9873
 
9342
9874
  #appendChunk(message) {
9343
9875
  const index = this.#findMessageIndex(message);
9344
9876
  if (index !== -1) {
9345
9877
  const existing = this.messages[index];
9346
- existing.text = mergeAgentMessageText(
9878
+ const nextText = mergeAgentMessageText(
9347
9879
  existing.text || '',
9348
9880
  message.text || ''
9349
9881
  );
9882
+ if (nextText !== existing.text) {
9883
+ existing.text = nextText;
9884
+ clearAgentMessageMarkdownCache(existing);
9885
+ }
9350
9886
  return;
9351
9887
  }
9352
9888
 
9353
- this.messages.push(this.#normalizeMessage({
9889
+ const nextMessage = this.#normalizeMessage({
9354
9890
  id: crypto.randomUUID(),
9355
9891
  streamKey: message.streamKey,
9356
9892
  role: message.role || 'assistant',
9357
9893
  kind: message.kind || 'message',
9358
9894
  text: message.text || '',
9359
9895
  createdAt: new Date().toISOString()
9360
- }));
9896
+ });
9897
+ clearAgentMessageMarkdownCache(nextMessage);
9898
+ this.messages.push(nextMessage);
9361
9899
  }
9362
9900
 
9363
9901
  #applySessionUpdate(update) {
@@ -9515,9 +10053,21 @@ class AgentTab {
9515
10053
  this.busy = typeof data.busy === 'boolean' ? data.busy : this.busy;
9516
10054
  this.errorMessage = data.errorMessage || this.errorMessage || '';
9517
10055
  this.currentModeId = data.currentModeId || this.currentModeId || '';
10056
+ this.availableModes = Array.isArray(data.availableModes)
10057
+ ? data.availableModes
10058
+ : this.availableModes;
10059
+ this.availableCommands = Array.isArray(data.availableCommands)
10060
+ ? data.availableCommands
10061
+ : this.availableCommands;
10062
+ this.configOptions = Array.isArray(data.configOptions)
10063
+ ? data.configOptions
10064
+ : this.configOptions;
9518
10065
  this.sessionCapabilities = normalizeAgentSessionCapabilities(
9519
10066
  data.sessionCapabilities || this.sessionCapabilities
9520
10067
  );
10068
+ if (data.usage) {
10069
+ this.usage = this.#normalizeUsageState(data.usage);
10070
+ }
9521
10071
  const nextSession = this.getLinkedSession();
9522
10072
  const nextSnapshot = JSON.stringify({
9523
10073
  runtimeId: this.runtimeId || '',
@@ -10484,6 +11034,19 @@ function mergeAgentMessageText(previousText, chunkText) {
10484
11034
  const chunk = String(chunkText || '');
10485
11035
  if (!previous) return chunk;
10486
11036
  if (!chunk) return previous;
11037
+ if (previous === chunk) return previous;
11038
+ if (chunk.startsWith(previous)) {
11039
+ return chunk;
11040
+ }
11041
+ if (previous.startsWith(chunk)) {
11042
+ return previous;
11043
+ }
11044
+ const maxOverlap = Math.min(previous.length, chunk.length, 2048);
11045
+ for (let overlap = maxOverlap; overlap >= 2; overlap -= 1) {
11046
+ if (previous.slice(-overlap) === chunk.slice(0, overlap)) {
11047
+ return `${previous}${chunk.slice(overlap)}`;
11048
+ }
11049
+ }
10487
11050
  if (/\s$/.test(previous) || /^\s/.test(chunk)) {
10488
11051
  return `${previous}${chunk}`;
10489
11052
  }
@@ -10880,6 +11443,194 @@ function getAgentTimelineItems(agentTab) {
10880
11443
  return items;
10881
11444
  }
10882
11445
 
11446
+ function getAgentTimelineItemKey(entry, absoluteIndex = 0) {
11447
+ if (!entry) {
11448
+ return `unknown:${absoluteIndex}`;
11449
+ }
11450
+ const order = Number.isFinite(entry.order) ? entry.order : -1;
11451
+ return `${entry.type}:${order}:${absoluteIndex}`;
11452
+ }
11453
+
11454
+ function clearAgentMessageMarkdownCache(message) {
11455
+ if (!message || typeof message !== 'object') {
11456
+ return;
11457
+ }
11458
+ delete message.markdownRenderSource;
11459
+ delete message.markdownRenderHtml;
11460
+ }
11461
+
11462
+ function getAgentMessageMarkdownCache(message) {
11463
+ if (!message || typeof message !== 'object') {
11464
+ return '';
11465
+ }
11466
+ const source = typeof message.text === 'string' ? message.text : '';
11467
+ if (
11468
+ typeof message.markdownRenderSource !== 'string'
11469
+ || message.markdownRenderSource !== source
11470
+ ) {
11471
+ return '';
11472
+ }
11473
+ return typeof message.markdownRenderHtml === 'string'
11474
+ ? message.markdownRenderHtml
11475
+ : '';
11476
+ }
11477
+
11478
+ function isAgentMessageStreaming(agentTab, message) {
11479
+ if (!agentTab || !message) {
11480
+ return false;
11481
+ }
11482
+ return !!(
11483
+ agentTab.busy
11484
+ && message.role === 'assistant'
11485
+ && message.kind === 'message'
11486
+ && message.streamKey
11487
+ && message.streamKey === agentTab.streamingAssistantStreamKey
11488
+ );
11489
+ }
11490
+
11491
+ function hashUiText(value) {
11492
+ const text = String(value || '');
11493
+ let hash = 2166136261;
11494
+ for (let index = 0; index < text.length; index += 1) {
11495
+ hash ^= text.charCodeAt(index);
11496
+ hash = Math.imul(hash, 16777619);
11497
+ }
11498
+ return (hash >>> 0).toString(16);
11499
+ }
11500
+
11501
+ function getAgentTimelineEntrySignature(entry) {
11502
+ if (!entry || typeof entry !== 'object') {
11503
+ return 'unknown';
11504
+ }
11505
+ const type = String(entry.type || '');
11506
+ const value = entry.value;
11507
+ if (type === 'message') {
11508
+ const attachments = Array.isArray(value?.attachments)
11509
+ ? value.attachments.map((attachment) => [
11510
+ attachment?.kind || '',
11511
+ attachment?.name || '',
11512
+ attachment?.path || '',
11513
+ attachment?.url || '',
11514
+ attachment?.size || 0,
11515
+ attachment?.lastModified || 0
11516
+ ])
11517
+ : [];
11518
+ return [
11519
+ type,
11520
+ value?.role || '',
11521
+ value?.kind || '',
11522
+ value?.createdAt || '',
11523
+ hashUiText(value?.text || ''),
11524
+ hashUiText(JSON.stringify(attachments))
11525
+ ].join(':');
11526
+ }
11527
+ return `${type}:${hashUiText(JSON.stringify(value || null))}`;
11528
+ }
11529
+
11530
+ function getAgentTimelineRenderSignature(agentTab, entry) {
11531
+ const base = getAgentTimelineEntrySignature(entry);
11532
+ if (entry?.type !== 'message') {
11533
+ return base;
11534
+ }
11535
+ return `${base}:${
11536
+ isAgentMessageStreaming(agentTab, entry.value)
11537
+ ? 'streaming'
11538
+ : 'settled'
11539
+ }`;
11540
+ }
11541
+
11542
+ function formatWorkspaceTabTitle(
11543
+ value,
11544
+ maxLength = WORKSPACE_TAB_TITLE_MAX_LENGTH
11545
+ ) {
11546
+ const text = String(value || '');
11547
+ const characters = Array.from(text);
11548
+ if (characters.length <= maxLength) {
11549
+ return text;
11550
+ }
11551
+ if (maxLength <= 3) {
11552
+ return '.'.repeat(Math.max(0, maxLength));
11553
+ }
11554
+ return `${characters.slice(0, maxLength - 3).join('')}...`;
11555
+ }
11556
+
11557
+ function getAgentTranscriptWindow(
11558
+ agentTab,
11559
+ totalCount = 0,
11560
+ options = {}
11561
+ ) {
11562
+ const total = Number.isFinite(totalCount)
11563
+ ? Math.max(0, totalCount)
11564
+ : 0;
11565
+ const windowSize = Math.min(total, AGENT_TRANSCRIPT_INITIAL_VISIBLE_BLOCKS);
11566
+ const latestStart = Math.max(0, total - windowSize);
11567
+ const latestWindow = {
11568
+ start: latestStart,
11569
+ end: total
11570
+ };
11571
+ if (!agentTab) {
11572
+ return latestWindow;
11573
+ }
11574
+ if (windowSize === 0) {
11575
+ agentTab.historyWindowStart = 0;
11576
+ agentTab.historyWindowEnd = 0;
11577
+ return { start: 0, end: 0 };
11578
+ }
11579
+ if (options.pinToBottom) {
11580
+ agentTab.historyWindowStart = latestWindow.start;
11581
+ agentTab.historyWindowEnd = latestWindow.end;
11582
+ return latestWindow;
11583
+ }
11584
+ let start = Number.isFinite(agentTab.historyWindowStart)
11585
+ ? Math.max(0, Math.floor(agentTab.historyWindowStart))
11586
+ : latestWindow.start;
11587
+ let end = Number.isFinite(agentTab.historyWindowEnd)
11588
+ ? Math.max(start, Math.floor(agentTab.historyWindowEnd))
11589
+ : latestWindow.end;
11590
+ if (end > total) {
11591
+ end = total;
11592
+ }
11593
+ if (end - start !== windowSize) {
11594
+ if (total <= windowSize) {
11595
+ start = 0;
11596
+ end = total;
11597
+ } else if (end >= total) {
11598
+ end = total;
11599
+ start = latestWindow.start;
11600
+ } else if (start <= 0) {
11601
+ start = 0;
11602
+ end = windowSize;
11603
+ } else {
11604
+ end = Math.min(total, start + windowSize);
11605
+ start = Math.max(0, end - windowSize);
11606
+ }
11607
+ }
11608
+ agentTab.historyWindowStart = start;
11609
+ agentTab.historyWindowEnd = end;
11610
+ return { start, end };
11611
+ }
11612
+
11613
+ function isAgentTranscriptWindowNearLatest(agentTab, totalCount = 0) {
11614
+ const total = Number.isFinite(totalCount)
11615
+ ? Math.max(0, totalCount)
11616
+ : 0;
11617
+ if (!agentTab) {
11618
+ return true;
11619
+ }
11620
+ if (
11621
+ !Number.isFinite(agentTab.historyWindowStart)
11622
+ || !Number.isFinite(agentTab.historyWindowEnd)
11623
+ || agentTab.historyWindowStart < 0
11624
+ || agentTab.historyWindowEnd < 0
11625
+ ) {
11626
+ return true;
11627
+ }
11628
+ return agentTab.historyWindowEnd >= Math.max(
11629
+ 0,
11630
+ total - AGENT_TRANSCRIPT_FOLLOW_LATEST_TOLERANCE
11631
+ );
11632
+ }
11633
+
10883
11634
  function normalizePlanStatusClass(status = '') {
10884
11635
  const value = String(status || '').toLowerCase();
10885
11636
  if (value === 'completed') return 'completed';
@@ -11700,119 +12451,6 @@ function escapeHtml(text) {
11700
12451
  .replaceAll("'", '&#39;');
11701
12452
  }
11702
12453
 
11703
- function renderAgentInlineMarkdown(text) {
11704
- const source = String(text || '');
11705
- const pattern = /(`[^`]+`)|(\[([^\]]+)\]\(([^)]+)\))/g;
11706
- let result = '';
11707
- let lastIndex = 0;
11708
- for (const match of source.matchAll(pattern)) {
11709
- const [token, codeToken, , linkLabel, linkHref] = match;
11710
- result += escapeHtml(source.slice(lastIndex, match.index));
11711
- if (codeToken) {
11712
- result += `<code>${escapeHtml(codeToken.slice(1, -1))}</code>`;
11713
- } else if (linkLabel && linkHref) {
11714
- result += `<a href="${escapeHtml(linkHref)}">${escapeHtml(linkLabel)}</a>`;
11715
- } else {
11716
- result += escapeHtml(token);
11717
- }
11718
- lastIndex = (match.index || 0) + token.length;
11719
- }
11720
- result += escapeHtml(source.slice(lastIndex));
11721
- return result;
11722
- }
11723
-
11724
- function normalizeAgentMessageText(text) {
11725
- return String(text || '')
11726
- .replace(/\r\n/g, '\n')
11727
- .replace(/([.!?`'")])([A-Z[`"])/g, '$1\n\n$2');
11728
- }
11729
-
11730
- function renderAgentMessageMarkdown(text) {
11731
- const source = normalizeAgentMessageText(text);
11732
- if (!source) return '';
11733
-
11734
- const lines = source.split('\n');
11735
- const blocks = [];
11736
- let paragraph = [];
11737
- let list = [];
11738
- let codeFence = null;
11739
- let codeLines = [];
11740
-
11741
- const flushParagraph = () => {
11742
- if (paragraph.length === 0) return;
11743
- blocks.push(
11744
- `<p>${paragraph.map(renderAgentInlineMarkdown).join('<br>')}</p>`
11745
- );
11746
- paragraph = [];
11747
- };
11748
-
11749
- const flushList = () => {
11750
- if (list.length === 0) return;
11751
- blocks.push(
11752
- `<ul>${list.map((item) => (
11753
- `<li>${renderAgentInlineMarkdown(item)}</li>`
11754
- )).join('')}</ul>`
11755
- );
11756
- list = [];
11757
- };
11758
-
11759
- const flushCode = () => {
11760
- if (codeFence === null) return;
11761
- const languageClass = codeFence
11762
- ? ` class="language-${escapeHtml(codeFence)}"`
11763
- : '';
11764
- blocks.push(
11765
- `<pre><code${languageClass}>${escapeHtml(
11766
- codeLines.join('\n')
11767
- )}</code></pre>`
11768
- );
11769
- codeFence = null;
11770
- codeLines = [];
11771
- };
11772
-
11773
- for (const line of lines) {
11774
- const fenceMatch = line.match(/^```(.*)$/);
11775
- if (fenceMatch) {
11776
- flushParagraph();
11777
- flushList();
11778
- if (codeFence === null) {
11779
- codeFence = fenceMatch[1].trim();
11780
- } else {
11781
- flushCode();
11782
- }
11783
- continue;
11784
- }
11785
-
11786
- if (codeFence !== null) {
11787
- codeLines.push(line);
11788
- continue;
11789
- }
11790
-
11791
- if (!line.trim()) {
11792
- flushParagraph();
11793
- flushList();
11794
- continue;
11795
- }
11796
-
11797
- const listMatch = line.match(/^[-*]\s+(.*)$/);
11798
- if (listMatch) {
11799
- flushParagraph();
11800
- list.push(listMatch[1]);
11801
- continue;
11802
- }
11803
-
11804
- flushList();
11805
- paragraph.push(line);
11806
- }
11807
-
11808
- flushParagraph();
11809
- flushList();
11810
- flushCode();
11811
-
11812
- const html = blocks.join('');
11813
- return DOMPurify.sanitize(html);
11814
- }
11815
-
11816
12454
  function truncateAgentDetail(text, limit = AGENT_MESSAGE_MAX_RENDER_BYTES) {
11817
12455
  const value = String(text || '');
11818
12456
  if (value.length <= limit) return value;
@@ -12929,9 +13567,20 @@ function upsertAgentTab(server, data) {
12929
13567
  const key = makeAgentTabKey(server.id, data.id);
12930
13568
  const existing = state.agentTabs.get(key);
12931
13569
  if (existing) {
12932
- existing.update(data);
13570
+ const hasLiveSocket = existing.socket?.readyState === WebSocket.OPEN;
13571
+ let shouldNotify = true;
13572
+ if (
13573
+ hasLiveSocket
13574
+ && !shouldApplyAuthoritativeAgentSnapshot(existing, data)
13575
+ ) {
13576
+ shouldNotify = existing.applyInventory(data);
13577
+ } else {
13578
+ existing.update(data);
13579
+ }
12933
13580
  existing.connect();
12934
- existing.notifyUi();
13581
+ if (shouldNotify) {
13582
+ existing.notifyUi();
13583
+ }
12935
13584
  return existing;
12936
13585
  }
12937
13586
  const agentTab = new AgentTab(data, server);
@@ -12939,6 +13588,43 @@ function upsertAgentTab(server, data) {
12939
13588
  return agentTab;
12940
13589
  }
12941
13590
 
13591
+ function getAgentMessageComparableSignature(message) {
13592
+ if (!message || typeof message !== 'object') {
13593
+ return '';
13594
+ }
13595
+ return JSON.stringify([
13596
+ message.id || '',
13597
+ message.order || 0,
13598
+ message.role || '',
13599
+ message.kind || '',
13600
+ message.streamKey || '',
13601
+ hashUiText(message.text || '')
13602
+ ]);
13603
+ }
13604
+
13605
+ function shouldApplyAuthoritativeAgentSnapshot(existing, data) {
13606
+ if (!existing || !data || typeof data !== 'object') {
13607
+ return false;
13608
+ }
13609
+ const incomingBusy = data.busy === true;
13610
+ if (incomingBusy) {
13611
+ return false;
13612
+ }
13613
+ if (!Array.isArray(data.messages)) {
13614
+ return true;
13615
+ }
13616
+ const currentMessages = Array.isArray(existing.messages)
13617
+ ? existing.messages
13618
+ : [];
13619
+ if (data.messages.length !== currentMessages.length) {
13620
+ return true;
13621
+ }
13622
+ const incomingLast = data.messages[data.messages.length - 1] || null;
13623
+ const currentLast = currentMessages[currentMessages.length - 1] || null;
13624
+ return getAgentMessageComparableSignature(incomingLast)
13625
+ !== getAgentMessageComparableSignature(currentLast);
13626
+ }
13627
+
12942
13628
  function upsertAgentInventoryTab(server, data) {
12943
13629
  const key = makeAgentTabKey(server.id, data.id);
12944
13630
  const existing = state.agentTabs.get(key);