tabminal 3.0.15 → 3.0.17

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
@@ -4264,23 +4264,38 @@ class EditorManager {
4264
4264
  if (!(body instanceof HTMLElement) || !message?.text) {
4265
4265
  return;
4266
4266
  }
4267
+ if (agentTab?.busy) {
4268
+ return;
4269
+ }
4270
+ if (isAgentMessageStreaming(agentTab, message)) {
4271
+ return;
4272
+ }
4267
4273
  const session = this.currentSession;
4268
4274
  if (!session) {
4269
4275
  return;
4270
4276
  }
4271
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
+
4272
4287
  const renderToken = `${Date.now()}:${Math.random()}`;
4273
4288
  body.dataset.markdownRenderToken = renderToken;
4274
4289
 
4275
4290
  try {
4276
4291
  const { renderer } = await loadMarkdownPreviewBundle();
4277
4292
  if (
4278
- !body.isConnected
4279
- || body.dataset.markdownRenderToken !== renderToken
4293
+ String(message.text || '') !== sourceText
4294
+ || isAgentMessageStreaming(agentTab, message)
4280
4295
  ) {
4281
4296
  return;
4282
4297
  }
4283
- const rendered = renderer.render(String(message.text || ''));
4298
+ const rendered = renderer.render(sourceText);
4284
4299
  const sanitized = DOMPurify.sanitize(rendered, {
4285
4300
  USE_PROFILES: {
4286
4301
  html: true,
@@ -4299,6 +4314,17 @@ class EditorManager {
4299
4314
  basePath,
4300
4315
  session
4301
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');
4302
4328
  body.replaceChildren(template.content);
4303
4329
  } catch {
4304
4330
  // Keep the lightweight fallback rendering.
@@ -5599,7 +5625,6 @@ class EditorManager {
5599
5625
  }
5600
5626
 
5601
5627
  renderAgentPanel(agentTab, options = {}) {
5602
- this.disposeAgentEmbeddedEditors();
5603
5628
  const previousLayout = this.captureAgentTranscriptLayout();
5604
5629
  const previousScrollTop = previousLayout?.scrollTop || 0;
5605
5630
  const wasNearBottom = this.isAgentTranscriptLayoutNearBottom(
@@ -5663,7 +5688,6 @@ class EditorManager {
5663
5688
 
5664
5689
  this.renderAgentComposerAttachments(agentTab);
5665
5690
 
5666
- this.agentTranscript.innerHTML = '';
5667
5691
  const timeline = getAgentTimelineItems(agentTab);
5668
5692
  const shouldPinToBottom = wasNearBottom || (
5669
5693
  agentTab.scrollToBottomOnNextRender
@@ -5682,45 +5706,66 @@ class EditorManager {
5682
5706
  transcriptWindow.end
5683
5707
  );
5684
5708
  if (timeline.length === 0) {
5685
- this.agentTranscript.appendChild(
5686
- this.buildAgentEmptyState(agentTab)
5687
- );
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
+ }
5688
5715
  } else {
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);
5722
+ }
5723
+ }
5724
+ const nextNodes = [];
5689
5725
  for (const [index, entry] of visibleTimeline.entries()) {
5690
5726
  const timelineIndex = transcriptWindow.start + index;
5691
- let node = null;
5692
- if (entry.type === 'message') {
5693
- node = this.buildAgentMessageNode(agentTab, entry.value);
5694
- } else if (entry.type === 'tool') {
5695
- node = this.buildAgentToolNode(agentTab, entry.value);
5696
- } else if (entry.type === 'permission') {
5697
- node = this.buildAgentPermissionNode(
5698
- agentTab,
5699
- entry.value
5700
- );
5701
- } else if (entry.type === 'plan') {
5702
- node = this.buildAgentPlanHistoryNode(
5703
- agentTab,
5704
- entry.value
5705
- );
5706
- }
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;
5707
5740
  if (node) {
5708
- node.dataset.timelineKey = getAgentTimelineItemKey(
5709
- entry,
5710
- timelineIndex
5711
- );
5741
+ existingByKey.delete(timelineKey);
5712
5742
  if (
5713
- timelineIndex > 0
5714
- && entry.type === 'message'
5715
- && String(entry.value?.role || '').toLowerCase()
5716
- === 'user'
5743
+ node.dataset.renderSignature !== renderSignature
5717
5744
  ) {
5718
- node.classList.add('agent-turn-start');
5745
+ this.disposeAgentTimelineNode(node);
5746
+ node = null;
5719
5747
  }
5720
- this.agentTranscript.appendChild(node);
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);
5721
5761
  }
5722
5762
  }
5763
+ for (const node of existingByKey.values()) {
5764
+ this.disposeAgentTimelineNode(node);
5765
+ }
5766
+ this.applyAgentTranscriptNodeList(nextNodes);
5723
5767
  }
5768
+ this.rebuildAgentEmbeddedTerminalRegistry();
5724
5769
  if (options.preserveTranscriptAnchor) {
5725
5770
  const restored = this.restoreAgentTranscriptAnchor(
5726
5771
  options.preserveTranscriptAnchor
@@ -5751,6 +5796,60 @@ class EditorManager {
5751
5796
  this.scheduleAgentTranscriptViewportUpdate(shouldPinToBottom);
5752
5797
  }
5753
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
+
5754
5853
  async loadOlderAgentTimeline(agentTab) {
5755
5854
  if (!agentTab || agentTab.historyWindowLoading) {
5756
5855
  return;
@@ -6141,9 +6240,23 @@ class EditorManager {
6141
6240
  message.role === 'assistant'
6142
6241
  && message.kind === 'message'
6143
6242
  ) {
6144
- body.classList.add('markdown');
6145
- body.innerHTML = renderAgentMessageMarkdown(message.text || '');
6146
- void this.enhanceAgentMarkdownBody(agentTab, message, body);
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
+ }
6147
6260
  } else {
6148
6261
  body.classList.add('plain');
6149
6262
  body.textContent = message.text || '';
@@ -6424,16 +6537,72 @@ class EditorManager {
6424
6537
 
6425
6538
  disposeAgentEmbeddedEditors() {
6426
6539
  this.agentEmbeddedTerminals.clear();
6427
- for (const disposable of this.agentEmbeddedEditors) {
6428
- try {
6429
- disposable.dispose();
6430
- } catch {
6431
- // Ignore embedded editor disposal failures.
6432
- }
6540
+ if (!this.agentTranscript) {
6541
+ this.agentEmbeddedEditors = [];
6542
+ return;
6543
+ }
6544
+ for (const node of Array.from(this.agentTranscript.children)) {
6545
+ this.disposeAgentTimelineNode(node);
6433
6546
  }
6434
6547
  this.agentEmbeddedEditors = [];
6435
6548
  }
6436
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
+
6437
6606
  buildAgentSectionBody(details, section) {
6438
6607
  if (
6439
6608
  section?.kind === 'diff'
@@ -6497,7 +6666,8 @@ class EditorManager {
6497
6666
  + '"JetBrains Mono", Menlo, Consolas, monospace'
6498
6667
  }
6499
6668
  );
6500
- this.agentEmbeddedEditors.push(editor, model);
6669
+ this.trackAgentTimelineDisposable(host, editor);
6670
+ this.trackAgentTimelineDisposable(host, model);
6501
6671
  details.addEventListener('toggle', () => {
6502
6672
  if (details.open) {
6503
6673
  requestAnimationFrame(() => {
@@ -6584,20 +6754,19 @@ class EditorManager {
6584
6754
  }
6585
6755
  });
6586
6756
  if (terminalId) {
6587
- if (!this.agentEmbeddedTerminals.has(terminalId)) {
6588
- this.agentEmbeddedTerminals.set(terminalId, []);
6589
- }
6590
- this.agentEmbeddedTerminals.get(terminalId).push({
6591
- meta,
6592
- header,
6593
- openButton,
6594
- terminalNode,
6595
- terminal: embeddedTerm,
6596
- fitAddon,
6757
+ host.__agentTerminalBinding = {
6758
+ terminalId,
6759
+ meta,
6760
+ header,
6761
+ openButton,
6762
+ terminalNode,
6763
+ terminal: embeddedTerm,
6764
+ fitAddon,
6597
6765
  layout: layoutTerminal
6598
- });
6766
+ };
6599
6767
  }
6600
- this.agentEmbeddedEditors.push(embeddedTerm, fitAddon);
6768
+ this.trackAgentTimelineDisposable(host, embeddedTerm);
6769
+ this.trackAgentTimelineDisposable(host, fitAddon);
6601
6770
 
6602
6771
  return host;
6603
6772
  }
@@ -6711,11 +6880,9 @@ class EditorManager {
6711
6880
  original: originalModel,
6712
6881
  modified: modifiedModel
6713
6882
  });
6714
- this.agentEmbeddedEditors.push(
6715
- diffEditor,
6716
- originalModel,
6717
- modifiedModel
6718
- );
6883
+ this.trackAgentTimelineDisposable(host, diffEditor);
6884
+ this.trackAgentTimelineDisposable(host, originalModel);
6885
+ this.trackAgentTimelineDisposable(host, modifiedModel);
6719
6886
  details.addEventListener('toggle', () => {
6720
6887
  if (details.open) {
6721
6888
  requestAnimationFrame(() => {
@@ -9088,6 +9255,7 @@ class AgentTab {
9088
9255
  this.historyWindowStart = -1;
9089
9256
  this.historyWindowEnd = -1;
9090
9257
  this.historyWindowLoading = false;
9258
+ this.streamingAssistantStreamKey = '';
9091
9259
  this.resumeSessions = [];
9092
9260
  this.resumeSessionsLoadedAt = 0;
9093
9261
  this.resumeSessionsPromise = null;
@@ -9295,9 +9463,23 @@ class AgentTab {
9295
9463
  break;
9296
9464
  case 'message_open':
9297
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
+ }
9298
9473
  break;
9299
9474
  case 'message_chunk':
9300
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
+ }
9301
9483
  break;
9302
9484
  case 'session_update':
9303
9485
  this.#applySessionUpdate(message.update || {});
@@ -9393,6 +9575,9 @@ class AgentTab {
9393
9575
  default:
9394
9576
  break;
9395
9577
  }
9578
+ if (!this.busy) {
9579
+ this.streamingAssistantStreamKey = '';
9580
+ }
9396
9581
  const shouldAutostartQueuedPrompt = (
9397
9582
  wasBusy
9398
9583
  && !this.busy
@@ -9646,7 +9831,7 @@ class AgentTab {
9646
9831
  const byId = this.messages.findIndex(
9647
9832
  (message) => message.id === candidate.id
9648
9833
  );
9649
- if (byId !== -1) return byId;
9834
+ return byId;
9650
9835
  }
9651
9836
  if (!candidate.streamKey) return -1;
9652
9837
  for (let index = this.messages.length - 1; index >= 0; index -= 1) {
@@ -9666,39 +9851,51 @@ class AgentTab {
9666
9851
  if (!message) return;
9667
9852
  const index = this.#findMessageIndex(message);
9668
9853
  if (index === -1) {
9669
- this.messages.push(this.#normalizeMessage(message));
9854
+ const nextMessage = this.#normalizeMessage(message);
9855
+ clearAgentMessageMarkdownCache(nextMessage);
9856
+ this.messages.push(nextMessage);
9670
9857
  return;
9671
9858
  }
9672
9859
 
9673
9860
  const previous = this.messages[index];
9674
9861
  const nextMessage = this.#normalizeMessage(message, previous.order);
9862
+ const mergedText = selectAgentMessageText(previous.text, nextMessage.text);
9675
9863
  this.messages[index] = {
9676
9864
  ...previous,
9677
9865
  ...nextMessage,
9678
9866
  createdAt: nextMessage.createdAt || previous.createdAt || '',
9679
- text: selectAgentMessageText(previous.text, nextMessage.text)
9867
+ text: mergedText
9680
9868
  };
9869
+ if (mergedText !== (previous.text || '')) {
9870
+ clearAgentMessageMarkdownCache(this.messages[index]);
9871
+ }
9681
9872
  }
9682
9873
 
9683
9874
  #appendChunk(message) {
9684
9875
  const index = this.#findMessageIndex(message);
9685
9876
  if (index !== -1) {
9686
9877
  const existing = this.messages[index];
9687
- existing.text = mergeAgentMessageText(
9878
+ const nextText = mergeAgentMessageText(
9688
9879
  existing.text || '',
9689
9880
  message.text || ''
9690
9881
  );
9882
+ if (nextText !== existing.text) {
9883
+ existing.text = nextText;
9884
+ clearAgentMessageMarkdownCache(existing);
9885
+ }
9691
9886
  return;
9692
9887
  }
9693
9888
 
9694
- this.messages.push(this.#normalizeMessage({
9889
+ const nextMessage = this.#normalizeMessage({
9695
9890
  id: crypto.randomUUID(),
9696
9891
  streamKey: message.streamKey,
9697
9892
  role: message.role || 'assistant',
9698
9893
  kind: message.kind || 'message',
9699
9894
  text: message.text || '',
9700
9895
  createdAt: new Date().toISOString()
9701
- }));
9896
+ });
9897
+ clearAgentMessageMarkdownCache(nextMessage);
9898
+ this.messages.push(nextMessage);
9702
9899
  }
9703
9900
 
9704
9901
  #applySessionUpdate(update) {
@@ -9856,9 +10053,21 @@ class AgentTab {
9856
10053
  this.busy = typeof data.busy === 'boolean' ? data.busy : this.busy;
9857
10054
  this.errorMessage = data.errorMessage || this.errorMessage || '';
9858
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;
9859
10065
  this.sessionCapabilities = normalizeAgentSessionCapabilities(
9860
10066
  data.sessionCapabilities || this.sessionCapabilities
9861
10067
  );
10068
+ if (data.usage) {
10069
+ this.usage = this.#normalizeUsageState(data.usage);
10070
+ }
9862
10071
  const nextSession = this.getLinkedSession();
9863
10072
  const nextSnapshot = JSON.stringify({
9864
10073
  runtimeId: this.runtimeId || '',
@@ -10825,6 +11034,19 @@ function mergeAgentMessageText(previousText, chunkText) {
10825
11034
  const chunk = String(chunkText || '');
10826
11035
  if (!previous) return chunk;
10827
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
+ }
10828
11050
  if (/\s$/.test(previous) || /^\s/.test(chunk)) {
10829
11051
  return `${previous}${chunk}`;
10830
11052
  }
@@ -11229,6 +11451,94 @@ function getAgentTimelineItemKey(entry, absoluteIndex = 0) {
11229
11451
  return `${entry.type}:${order}:${absoluteIndex}`;
11230
11452
  }
11231
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
+
11232
11542
  function formatWorkspaceTabTitle(
11233
11543
  value,
11234
11544
  maxLength = WORKSPACE_TAB_TITLE_MAX_LENGTH
@@ -12141,119 +12451,6 @@ function escapeHtml(text) {
12141
12451
  .replaceAll("'", '&#39;');
12142
12452
  }
12143
12453
 
12144
- function renderAgentInlineMarkdown(text) {
12145
- const source = String(text || '');
12146
- const pattern = /(`[^`]+`)|(\[([^\]]+)\]\(([^)]+)\))/g;
12147
- let result = '';
12148
- let lastIndex = 0;
12149
- for (const match of source.matchAll(pattern)) {
12150
- const [token, codeToken, , linkLabel, linkHref] = match;
12151
- result += escapeHtml(source.slice(lastIndex, match.index));
12152
- if (codeToken) {
12153
- result += `<code>${escapeHtml(codeToken.slice(1, -1))}</code>`;
12154
- } else if (linkLabel && linkHref) {
12155
- result += `<a href="${escapeHtml(linkHref)}">${escapeHtml(linkLabel)}</a>`;
12156
- } else {
12157
- result += escapeHtml(token);
12158
- }
12159
- lastIndex = (match.index || 0) + token.length;
12160
- }
12161
- result += escapeHtml(source.slice(lastIndex));
12162
- return result;
12163
- }
12164
-
12165
- function normalizeAgentMessageText(text) {
12166
- return String(text || '')
12167
- .replace(/\r\n/g, '\n')
12168
- .replace(/([.!?`'")])([A-Z[`"])/g, '$1\n\n$2');
12169
- }
12170
-
12171
- function renderAgentMessageMarkdown(text) {
12172
- const source = normalizeAgentMessageText(text);
12173
- if (!source) return '';
12174
-
12175
- const lines = source.split('\n');
12176
- const blocks = [];
12177
- let paragraph = [];
12178
- let list = [];
12179
- let codeFence = null;
12180
- let codeLines = [];
12181
-
12182
- const flushParagraph = () => {
12183
- if (paragraph.length === 0) return;
12184
- blocks.push(
12185
- `<p>${paragraph.map(renderAgentInlineMarkdown).join('<br>')}</p>`
12186
- );
12187
- paragraph = [];
12188
- };
12189
-
12190
- const flushList = () => {
12191
- if (list.length === 0) return;
12192
- blocks.push(
12193
- `<ul>${list.map((item) => (
12194
- `<li>${renderAgentInlineMarkdown(item)}</li>`
12195
- )).join('')}</ul>`
12196
- );
12197
- list = [];
12198
- };
12199
-
12200
- const flushCode = () => {
12201
- if (codeFence === null) return;
12202
- const languageClass = codeFence
12203
- ? ` class="language-${escapeHtml(codeFence)}"`
12204
- : '';
12205
- blocks.push(
12206
- `<pre><code${languageClass}>${escapeHtml(
12207
- codeLines.join('\n')
12208
- )}</code></pre>`
12209
- );
12210
- codeFence = null;
12211
- codeLines = [];
12212
- };
12213
-
12214
- for (const line of lines) {
12215
- const fenceMatch = line.match(/^```(.*)$/);
12216
- if (fenceMatch) {
12217
- flushParagraph();
12218
- flushList();
12219
- if (codeFence === null) {
12220
- codeFence = fenceMatch[1].trim();
12221
- } else {
12222
- flushCode();
12223
- }
12224
- continue;
12225
- }
12226
-
12227
- if (codeFence !== null) {
12228
- codeLines.push(line);
12229
- continue;
12230
- }
12231
-
12232
- if (!line.trim()) {
12233
- flushParagraph();
12234
- flushList();
12235
- continue;
12236
- }
12237
-
12238
- const listMatch = line.match(/^[-*]\s+(.*)$/);
12239
- if (listMatch) {
12240
- flushParagraph();
12241
- list.push(listMatch[1]);
12242
- continue;
12243
- }
12244
-
12245
- flushList();
12246
- paragraph.push(line);
12247
- }
12248
-
12249
- flushParagraph();
12250
- flushList();
12251
- flushCode();
12252
-
12253
- const html = blocks.join('');
12254
- return DOMPurify.sanitize(html);
12255
- }
12256
-
12257
12454
  function truncateAgentDetail(text, limit = AGENT_MESSAGE_MAX_RENDER_BYTES) {
12258
12455
  const value = String(text || '');
12259
12456
  if (value.length <= limit) return value;
@@ -13370,9 +13567,20 @@ function upsertAgentTab(server, data) {
13370
13567
  const key = makeAgentTabKey(server.id, data.id);
13371
13568
  const existing = state.agentTabs.get(key);
13372
13569
  if (existing) {
13373
- 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
+ }
13374
13580
  existing.connect();
13375
- existing.notifyUi();
13581
+ if (shouldNotify) {
13582
+ existing.notifyUi();
13583
+ }
13376
13584
  return existing;
13377
13585
  }
13378
13586
  const agentTab = new AgentTab(data, server);
@@ -13380,6 +13588,43 @@ function upsertAgentTab(server, data) {
13380
13588
  return agentTab;
13381
13589
  }
13382
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
+
13383
13628
  function upsertAgentInventoryTab(server, data) {
13384
13629
  const key = makeAgentTabKey(server.id, data.id);
13385
13630
  const existing = state.agentTabs.get(key);