tabminal 3.0.17 → 3.0.18

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": "tabminal",
3
- "version": "3.0.17",
3
+ "version": "3.0.18",
4
4
  "description": "Tab(ter)minal, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -108,6 +108,8 @@ const FILE_VERSION_CHECK_INTERVAL_MS = 3000;
108
108
  const AGENT_TRANSCRIPT_INITIAL_VISIBLE_BLOCKS = 100;
109
109
  const AGENT_TRANSCRIPT_WINDOW_STEP = 50;
110
110
  const AGENT_TRANSCRIPT_FOLLOW_LATEST_TOLERANCE = 5;
111
+ const AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS = 300;
112
+ const AGENT_TRANSCRIPT_AUTH_SYNC_DEBOUNCE_MS = 300;
111
113
  const WORKSPACE_TAB_TITLE_MAX_LENGTH = 20;
112
114
  const MAIN_SERVER_ID = 'main';
113
115
  const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
@@ -1028,6 +1030,7 @@ class EditorManager {
1028
1030
  this.agentEmbeddedEditors = [];
1029
1031
  this.agentEmbeddedTerminals = new Map();
1030
1032
  this.agentTranscriptLayout = null;
1033
+ this.agentRenderQueue = new Map();
1031
1034
  this.pdfPreviewState = {
1032
1035
  path: '',
1033
1036
  sessionKey: '',
@@ -1063,7 +1066,6 @@ class EditorManager {
1063
1066
  this.initMonaco();
1064
1067
  this.loadIconMap();
1065
1068
  this.agentTimestampTimer = window.setInterval(() => {
1066
- this.refreshAgentTimelineTimestamps();
1067
1069
  this.refreshAgentUsageHud();
1068
1070
  }, 1000);
1069
1071
  this.fileVersionCheckTimer = window.setInterval(() => {
@@ -4264,12 +4266,6 @@ class EditorManager {
4264
4266
  if (!(body instanceof HTMLElement) || !message?.text) {
4265
4267
  return;
4266
4268
  }
4267
- if (agentTab?.busy) {
4268
- return;
4269
- }
4270
- if (isAgentMessageStreaming(agentTab, message)) {
4271
- return;
4272
- }
4273
4269
  const session = this.currentSession;
4274
4270
  if (!session) {
4275
4271
  return;
@@ -4289,10 +4285,7 @@ class EditorManager {
4289
4285
 
4290
4286
  try {
4291
4287
  const { renderer } = await loadMarkdownPreviewBundle();
4292
- if (
4293
- String(message.text || '') !== sourceText
4294
- || isAgentMessageStreaming(agentTab, message)
4295
- ) {
4288
+ if (String(message.text || '') !== sourceText) {
4296
4289
  return;
4297
4290
  }
4298
4291
  const rendered = renderer.render(sourceText);
@@ -5624,13 +5617,133 @@ class EditorManager {
5624
5617
  removeAgentTab(agentTabKey);
5625
5618
  }
5626
5619
 
5627
- renderAgentPanel(agentTab, options = {}) {
5628
- const previousLayout = this.captureAgentTranscriptLayout();
5629
- const previousScrollTop = previousLayout?.scrollTop || 0;
5630
- const wasNearBottom = this.isAgentTranscriptLayoutNearBottom(
5631
- previousLayout,
5632
- 36
5633
- );
5620
+ getOrCreateAgentRenderState(agentTabKey) {
5621
+ let renderState = this.agentRenderQueue.get(agentTabKey);
5622
+ if (!renderState) {
5623
+ renderState = {
5624
+ timer: 0,
5625
+ inFlight: false,
5626
+ rerenderRequested: false,
5627
+ full: false,
5628
+ authoritativeSync: false,
5629
+ delayMs: 0,
5630
+ dirtyKeys: new Set()
5631
+ };
5632
+ this.agentRenderQueue.set(agentTabKey, renderState);
5633
+ }
5634
+ return renderState;
5635
+ }
5636
+
5637
+ clearScheduledAgentPanelRender(agentTabKey) {
5638
+ const renderState = this.agentRenderQueue.get(agentTabKey);
5639
+ if (!renderState) {
5640
+ return;
5641
+ }
5642
+ if (renderState.timer) {
5643
+ clearTimeout(renderState.timer);
5644
+ }
5645
+ this.agentRenderQueue.delete(agentTabKey);
5646
+ }
5647
+
5648
+ scheduleQueuedAgentPanelRender(agentTabKey, delayMs = 0) {
5649
+ const renderState = this.agentRenderQueue.get(agentTabKey);
5650
+ if (!renderState) {
5651
+ return;
5652
+ }
5653
+ if (renderState.timer) {
5654
+ clearTimeout(renderState.timer);
5655
+ }
5656
+ renderState.delayMs = Math.max(0, Math.floor(delayMs));
5657
+ renderState.timer = window.setTimeout(() => {
5658
+ renderState.timer = 0;
5659
+ void this.flushQueuedAgentPanelRender(agentTabKey);
5660
+ }, renderState.delayMs);
5661
+ }
5662
+
5663
+ scheduleAgentPanelRender(agentTab, options = {}) {
5664
+ if (!agentTab?.key) {
5665
+ return;
5666
+ }
5667
+ const renderState = this.getOrCreateAgentRenderState(agentTab.key);
5668
+ if (options.full) {
5669
+ renderState.full = true;
5670
+ }
5671
+ if (options.authoritativeSync) {
5672
+ renderState.authoritativeSync = true;
5673
+ }
5674
+ if (options.dirtyKey) {
5675
+ renderState.dirtyKeys.add(String(options.dirtyKey));
5676
+ }
5677
+ if (renderState.inFlight) {
5678
+ renderState.rerenderRequested = true;
5679
+ }
5680
+ const delayMs = renderState.full
5681
+ ? 0
5682
+ : (
5683
+ Number.isFinite(options.delayMs)
5684
+ ? options.delayMs
5685
+ : AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS
5686
+ );
5687
+ this.scheduleQueuedAgentPanelRender(agentTab.key, delayMs);
5688
+ }
5689
+
5690
+ async flushQueuedAgentPanelRender(agentTabKey) {
5691
+ const renderState = this.agentRenderQueue.get(agentTabKey);
5692
+ if (!renderState) {
5693
+ return;
5694
+ }
5695
+ if (renderState.inFlight) {
5696
+ renderState.rerenderRequested = true;
5697
+ return;
5698
+ }
5699
+ renderState.inFlight = true;
5700
+ const pendingFull = renderState.full;
5701
+ const pendingAuthoritativeSync = renderState.authoritativeSync;
5702
+ renderState.full = false;
5703
+ renderState.authoritativeSync = false;
5704
+ renderState.rerenderRequested = false;
5705
+ renderState.dirtyKeys.clear();
5706
+ try {
5707
+ const agentTab = state.agentTabs.get(agentTabKey);
5708
+ if (!agentTab) {
5709
+ return;
5710
+ }
5711
+ if (isAgentTabVisible(agentTab)) {
5712
+ if (pendingFull) {
5713
+ this.renderAgentPanel(agentTab, {
5714
+ reason: 'queued-full'
5715
+ });
5716
+ } else {
5717
+ this.renderAgentTranscript(agentTab, {
5718
+ reason: 'queued-transcript'
5719
+ });
5720
+ }
5721
+ }
5722
+ if (pendingAuthoritativeSync && agentTab.server?.isAuthenticated) {
5723
+ try {
5724
+ await syncAgentsForServer(agentTab.server, { force: true });
5725
+ } catch {
5726
+ // Ignore transient authority sync failures. The next
5727
+ // heartbeat or state refresh will reconcile.
5728
+ }
5729
+ }
5730
+ } finally {
5731
+ renderState.inFlight = false;
5732
+ if (
5733
+ renderState.full
5734
+ || renderState.authoritativeSync
5735
+ || renderState.dirtyKeys.size > 0
5736
+ || renderState.rerenderRequested
5737
+ ) {
5738
+ const delayMs = renderState.full
5739
+ ? 0
5740
+ : AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS;
5741
+ this.scheduleQueuedAgentPanelRender(agentTabKey, delayMs);
5742
+ }
5743
+ }
5744
+ }
5745
+
5746
+ renderAgentPanelChrome(agentTab) {
5634
5747
  this.agentHeader.textContent = '';
5635
5748
  this.agentMeta.textContent = '';
5636
5749
  this.renderAgentUsageHud(agentTab);
@@ -5687,7 +5800,25 @@ class EditorManager {
5687
5800
  }
5688
5801
 
5689
5802
  this.renderAgentComposerAttachments(agentTab);
5803
+ this.agentTools.innerHTML = '';
5804
+ this.agentTools.style.display = 'none';
5805
+ this.agentPermissions.innerHTML = '';
5806
+ this.agentPermissions.style.display = 'none';
5690
5807
 
5808
+ this.agentPrompt.disabled = false;
5809
+ this.setAgentPromptValue(agentTab.promptDraft || '', agentTab);
5810
+ this.agentPrompt.placeholder = buildAgentPromptPlaceholder(agentTab);
5811
+ this.updateAgentComposerActions(agentTab);
5812
+ this.refreshAgentUsageHud();
5813
+ }
5814
+
5815
+ renderAgentTranscript(agentTab, options = {}) {
5816
+ const previousLayout = this.captureAgentTranscriptLayout();
5817
+ const previousScrollTop = previousLayout?.scrollTop || 0;
5818
+ const wasNearBottom = this.isAgentTranscriptLayoutNearBottom(
5819
+ previousLayout,
5820
+ 36
5821
+ );
5691
5822
  const timeline = getAgentTimelineItems(agentTab);
5692
5823
  const shouldPinToBottom = wasNearBottom || (
5693
5824
  agentTab.scrollToBottomOnNextRender
@@ -5782,20 +5913,14 @@ class EditorManager {
5782
5913
  }
5783
5914
  this.updateAgentScrollBottomButton();
5784
5915
  this.rememberAgentTranscriptLayout();
5785
- this.agentTools.innerHTML = '';
5786
- this.agentTools.style.display = 'none';
5787
- this.agentPermissions.innerHTML = '';
5788
- this.agentPermissions.style.display = 'none';
5789
-
5790
- this.agentPrompt.disabled = false;
5791
- this.setAgentPromptValue(agentTab.promptDraft || '', agentTab);
5792
- this.agentPrompt.placeholder = buildAgentPromptPlaceholder(agentTab);
5793
- this.updateAgentComposerActions(agentTab);
5794
- this.refreshAgentTimelineTimestamps();
5795
- this.refreshAgentUsageHud();
5796
5916
  this.scheduleAgentTranscriptViewportUpdate(shouldPinToBottom);
5797
5917
  }
5798
5918
 
5919
+ renderAgentPanel(agentTab, options = {}) {
5920
+ this.renderAgentPanelChrome(agentTab);
5921
+ this.renderAgentTranscript(agentTab, options);
5922
+ }
5923
+
5799
5924
  buildAgentTimelineNode(agentTab, entry, timelineIndex) {
5800
5925
  if (!entry) {
5801
5926
  return null;
@@ -5939,20 +6064,6 @@ class EditorManager {
5939
6064
  }
5940
6065
  }
5941
6066
 
5942
- refreshAgentTimelineTimestamps() {
5943
- if (!this.agentContainer || this.agentContainer.style.display === 'none') {
5944
- return;
5945
- }
5946
- const timestamps = this.agentContainer.querySelectorAll(
5947
- '.agent-message-time[data-created-at]'
5948
- );
5949
- for (const node of timestamps) {
5950
- const createdAt = String(node.dataset.createdAt || '').trim();
5951
- if (!createdAt) continue;
5952
- node.textContent = getAgentMessageTimeLabel({ createdAt });
5953
- }
5954
- }
5955
-
5956
6067
  refreshAgentUsageHud() {
5957
6068
  const activeTab = getActiveAgentTab();
5958
6069
  if (!activeTab || !this.agentUsageHud) return;
@@ -6192,9 +6303,7 @@ class EditorManager {
6192
6303
  const item = document.createElement('div');
6193
6304
  item.className = 'agent-message agent-plan-history';
6194
6305
  item.appendChild(buildAgentTimelineHeader(
6195
- buildAgentTimelineRoleLabel(agentTab, 'plan'),
6196
- getAgentMessageTimeLabel(planEntry),
6197
- planEntry.createdAt || ''
6306
+ buildAgentTimelineRoleLabel(agentTab, 'plan')
6198
6307
  ));
6199
6308
  const body = document.createElement('div');
6200
6309
  body.className = 'agent-plan-history-body';
@@ -6222,9 +6331,7 @@ class EditorManager {
6222
6331
  item.className = `agent-message ${message.role} ${message.kind}`;
6223
6332
 
6224
6333
  item.appendChild(buildAgentTimelineHeader(
6225
- getAgentMessageRoleLabel(agentTab, message),
6226
- getAgentMessageTimeLabel(message),
6227
- message.createdAt || ''
6334
+ getAgentMessageRoleLabel(agentTab, message)
6228
6335
  ));
6229
6336
  const attachments = buildAgentMessageAttachmentsNode(
6230
6337
  message.attachments
@@ -6241,11 +6348,7 @@ class EditorManager {
6241
6348
  && message.kind === 'message'
6242
6349
  ) {
6243
6350
  const cachedMarkdown = getAgentMessageMarkdownCache(message);
6244
- if (
6245
- !agentTab?.busy
6246
- && !isAgentMessageStreaming(agentTab, message)
6247
- && cachedMarkdown
6248
- ) {
6351
+ if (cachedMarkdown) {
6249
6352
  body.classList.add('markdown');
6250
6353
  body.innerHTML = cachedMarkdown;
6251
6354
  } else {
@@ -6275,9 +6378,7 @@ class EditorManager {
6275
6378
  node.className = `agent-tool-call state-${toolStatusClass}`;
6276
6379
 
6277
6380
  node.appendChild(buildAgentTimelineHeader(
6278
- buildAgentTimelineRoleLabel(agentTab, 'tool'),
6279
- getAgentMessageTimeLabel(toolCall),
6280
- toolCall.createdAt || ''
6381
+ buildAgentTimelineRoleLabel(agentTab, 'tool')
6281
6382
  ));
6282
6383
 
6283
6384
  const header = document.createElement('div');
@@ -6384,9 +6485,7 @@ class EditorManager {
6384
6485
  permission.status === 'pending'
6385
6486
  ? 'permission request'
6386
6487
  : 'permission'
6387
- ),
6388
- getAgentMessageTimeLabel(permission),
6389
- permission.createdAt || ''
6488
+ )
6390
6489
  ));
6391
6490
 
6392
6491
  const titleRow = document.createElement('div');
@@ -9270,11 +9369,32 @@ class AgentTab {
9270
9369
  ) || null;
9271
9370
  }
9272
9371
 
9273
- notifyUi() {
9372
+ notifyUi(options = {}) {
9274
9373
  const session = this.getLinkedSession();
9275
9374
  if (!session) return;
9276
- session.updateTabUI();
9277
- refreshWorkspaceIfSessionActive(session);
9375
+ const shouldUpdateTabs = options.updateTabs !== false;
9376
+ if (shouldUpdateTabs) {
9377
+ session.updateTabUI();
9378
+ }
9379
+ if (state.activeSessionKey !== session.key) {
9380
+ return;
9381
+ }
9382
+ if (editorManager.currentSession?.key !== session.key) {
9383
+ editorManager.switchTo(session);
9384
+ return;
9385
+ }
9386
+ if (shouldUpdateTabs) {
9387
+ editorManager.renderEditorTabs();
9388
+ }
9389
+ if (editorManager.getActiveWorkspaceTabKey(session) !== this.key) {
9390
+ return;
9391
+ }
9392
+ editorManager.scheduleAgentPanelRender(this, {
9393
+ full: options.full !== false,
9394
+ delayMs: options.delayMs,
9395
+ dirtyKey: options.dirtyKey || '',
9396
+ authoritativeSync: !!options.authoritativeSync
9397
+ });
9278
9398
  }
9279
9399
 
9280
9400
  update(data) {
@@ -9456,10 +9576,14 @@ class AgentTab {
9456
9576
 
9457
9577
  handleMessage(message) {
9458
9578
  const wasBusy = this.busy;
9579
+ let notifyOptions = { full: true };
9459
9580
  switch (message.type) {
9460
9581
  case 'snapshot':
9461
9582
  this.update(message.tab || {});
9462
9583
  this.scrollToBottomOnNextRender = true;
9584
+ notifyOptions = {
9585
+ full: true
9586
+ };
9463
9587
  break;
9464
9588
  case 'message_open':
9465
9589
  this.#upsertMessage(message.message);
@@ -9470,6 +9594,12 @@ class AgentTab {
9470
9594
  ) {
9471
9595
  this.streamingAssistantStreamKey = message.message.streamKey;
9472
9596
  }
9597
+ notifyOptions = {
9598
+ full: false,
9599
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
9600
+ dirtyKey: this.#getMessageRenderKey(message.message),
9601
+ updateTabs: false
9602
+ };
9473
9603
  break;
9474
9604
  case 'message_chunk':
9475
9605
  this.#appendChunk(message);
@@ -9480,9 +9610,15 @@ class AgentTab {
9480
9610
  ) {
9481
9611
  this.streamingAssistantStreamKey = message.streamKey;
9482
9612
  }
9613
+ notifyOptions = {
9614
+ full: false,
9615
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
9616
+ dirtyKey: this.#getMessageRenderKey(message),
9617
+ updateTabs: false
9618
+ };
9483
9619
  break;
9484
9620
  case 'session_update':
9485
- this.#applySessionUpdate(message.update || {});
9621
+ notifyOptions = this.#applySessionUpdate(message.update || {});
9486
9622
  if (message.tab?.currentModeId || message.tab?.modeId) {
9487
9623
  this.currentModeId = message.tab.currentModeId
9488
9624
  || message.tab.modeId;
@@ -9511,6 +9647,14 @@ class AgentTab {
9511
9647
  )
9512
9648
  });
9513
9649
  }
9650
+ notifyOptions = {
9651
+ full: false,
9652
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
9653
+ dirtyKey: this.#getPermissionRenderKey(
9654
+ message.permission?.id
9655
+ ),
9656
+ updateTabs: false
9657
+ };
9514
9658
  break;
9515
9659
  case 'permission_resolved': {
9516
9660
  const permission = this.permissions.get(message.permissionId);
@@ -9520,6 +9664,14 @@ class AgentTab {
9520
9664
  || permission.selectedOptionId
9521
9665
  || '';
9522
9666
  }
9667
+ notifyOptions = {
9668
+ full: false,
9669
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
9670
+ dirtyKey: this.#getPermissionRenderKey(
9671
+ message.permissionId
9672
+ ),
9673
+ updateTabs: false
9674
+ };
9523
9675
  break;
9524
9676
  }
9525
9677
  case 'terminal_update':
@@ -9559,18 +9711,22 @@ class AgentTab {
9559
9711
  return;
9560
9712
  }
9561
9713
  }
9714
+ notifyOptions = { full: true };
9562
9715
  break;
9563
9716
  case 'usage_state':
9564
9717
  this.usage = this.#normalizeUsageState(message.usage);
9718
+ notifyOptions = { full: true };
9565
9719
  break;
9566
9720
  case 'status':
9567
9721
  this.status = message.status || this.status;
9568
9722
  this.busy = !!message.busy;
9569
9723
  this.errorMessage = message.errorMessage || '';
9724
+ notifyOptions = { full: true };
9570
9725
  break;
9571
9726
  case 'complete':
9572
9727
  this.status = message.status || 'ready';
9573
9728
  this.busy = !!message.busy;
9729
+ notifyOptions = { full: true };
9574
9730
  break;
9575
9731
  default:
9576
9732
  break;
@@ -9611,7 +9767,15 @@ class AgentTab {
9611
9767
  this.needsAttention = false;
9612
9768
  }
9613
9769
  this.#syncBusyWatchdog();
9614
- this.notifyUi();
9770
+ if (wasBusy && !this.busy) {
9771
+ notifyOptions = {
9772
+ ...notifyOptions,
9773
+ full: true,
9774
+ authoritativeSync: true,
9775
+ delayMs: AGENT_TRANSCRIPT_AUTH_SYNC_DEBOUNCE_MS
9776
+ };
9777
+ }
9778
+ this.notifyUi(notifyOptions);
9615
9779
  if (shouldAutostartQueuedPrompt) {
9616
9780
  this.lastCompletedRunCounter = this.runCounter;
9617
9781
  void drainQueuedAgentPrompt(this);
@@ -9825,13 +9989,39 @@ class AgentTab {
9825
9989
  return this.timelineCounter;
9826
9990
  }
9827
9991
 
9992
+ #getMessageRenderKey(message = {}) {
9993
+ const role = String(message?.role || 'assistant');
9994
+ const kind = String(message?.kind || 'message');
9995
+ const identity = String(
9996
+ message?.id
9997
+ || message?.streamKey
9998
+ || ''
9999
+ ).trim();
10000
+ if (!identity) {
10001
+ return '';
10002
+ }
10003
+ return `message:${role}:${kind}:${identity}`;
10004
+ }
10005
+
10006
+ #getToolRenderKey(toolCallId = '') {
10007
+ const identity = String(toolCallId || '').trim();
10008
+ return identity ? `tool:${identity}` : '';
10009
+ }
10010
+
10011
+ #getPermissionRenderKey(permissionId = '') {
10012
+ const identity = String(permissionId || '').trim();
10013
+ return identity ? `permission:${identity}` : '';
10014
+ }
10015
+
9828
10016
  #findMessageIndex(candidate) {
9829
10017
  if (!candidate) return -1;
9830
10018
  if (candidate.id) {
9831
10019
  const byId = this.messages.findIndex(
9832
10020
  (message) => message.id === candidate.id
9833
10021
  );
9834
- return byId;
10022
+ if (byId !== -1) {
10023
+ return byId;
10024
+ }
9835
10025
  }
9836
10026
  if (!candidate.streamKey) return -1;
9837
10027
  for (let index = this.messages.length - 1; index >= 0; index -= 1) {
@@ -9859,7 +10049,12 @@ class AgentTab {
9859
10049
 
9860
10050
  const previous = this.messages[index];
9861
10051
  const nextMessage = this.#normalizeMessage(message, previous.order);
9862
- const mergedText = selectAgentMessageText(previous.text, nextMessage.text);
10052
+ const mergedText = (
10053
+ !previous.id
10054
+ && nextMessage.id
10055
+ ? (nextMessage.text || '')
10056
+ : selectAgentMessageText(previous.text, nextMessage.text)
10057
+ );
9863
10058
  this.messages[index] = {
9864
10059
  ...previous,
9865
10060
  ...nextMessage,
@@ -9883,16 +10078,22 @@ class AgentTab {
9883
10078
  existing.text = nextText;
9884
10079
  clearAgentMessageMarkdownCache(existing);
9885
10080
  }
10081
+ if (Number.isFinite(message?.order)) {
10082
+ existing.order = message.order;
10083
+ }
9886
10084
  return;
9887
10085
  }
9888
10086
 
9889
10087
  const nextMessage = this.#normalizeMessage({
9890
- id: crypto.randomUUID(),
10088
+ id: typeof message.id === 'string'
10089
+ ? message.id
10090
+ : '',
9891
10091
  streamKey: message.streamKey,
9892
10092
  role: message.role || 'assistant',
9893
10093
  kind: message.kind || 'message',
9894
10094
  text: message.text || '',
9895
- createdAt: new Date().toISOString()
10095
+ createdAt: new Date().toISOString(),
10096
+ order: message.order
9896
10097
  });
9897
10098
  clearAgentMessageMarkdownCache(nextMessage);
9898
10099
  this.messages.push(nextMessage);
@@ -9908,28 +10109,38 @@ class AgentTab {
9908
10109
  this.#normalizeTimelineEntry(update, previous?.order)
9909
10110
  );
9910
10111
  }
9911
- break;
10112
+ return {
10113
+ full: false,
10114
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
10115
+ dirtyKey: this.#getToolRenderKey(update.toolCallId),
10116
+ updateTabs: false
10117
+ };
9912
10118
  case 'tool_call_update': {
9913
10119
  const previous = this.toolCalls.get(update.toolCallId) || {};
9914
10120
  this.toolCalls.set(update.toolCallId, {
9915
10121
  ...previous,
9916
10122
  ...this.#normalizeTimelineEntry(update, previous.order)
9917
10123
  });
9918
- break;
10124
+ return {
10125
+ full: false,
10126
+ delayMs: AGENT_TRANSCRIPT_RENDER_DEBOUNCE_MS,
10127
+ dirtyKey: this.#getToolRenderKey(update.toolCallId),
10128
+ updateTabs: false
10129
+ };
9919
10130
  }
9920
10131
  case 'current_mode_update':
9921
10132
  this.currentModeId = update.currentModeId || update.modeId || '';
9922
- break;
10133
+ return { full: true };
9923
10134
  case 'available_commands_update':
9924
10135
  this.availableCommands = Array.isArray(update.availableCommands)
9925
10136
  ? update.availableCommands
9926
10137
  : [];
9927
- break;
10138
+ return { full: true };
9928
10139
  case 'config_option_update':
9929
10140
  this.configOptions = Array.isArray(update.configOptions)
9930
10141
  ? update.configOptions
9931
10142
  : [];
9932
- break;
10143
+ return { full: true };
9933
10144
  case 'plan':
9934
10145
  this.#applyPlanState(
9935
10146
  Array.isArray(update.entries)
@@ -9938,22 +10149,22 @@ class AgentTab {
9938
10149
  )
9939
10150
  : []
9940
10151
  );
9941
- break;
10152
+ return { full: true };
9942
10153
  case 'usage_update':
9943
10154
  this.usage = this.#normalizeUsageState({
9944
10155
  ...(this.usage || {}),
9945
10156
  ...update
9946
10157
  });
9947
- break;
10158
+ return { full: true };
9948
10159
  case 'session_info_update':
9949
10160
  if (typeof update.title === 'string') {
9950
10161
  this.title = update.title;
9951
10162
  } else if (update.title === null) {
9952
10163
  this.title = '';
9953
10164
  }
9954
- break;
10165
+ return { full: true };
9955
10166
  default:
9956
- break;
10167
+ return { full: true };
9957
10168
  }
9958
10169
  }
9959
10170
 
@@ -11366,9 +11577,7 @@ function getAgentMessageTimeLabel(message) {
11366
11577
  }
11367
11578
 
11368
11579
  function buildAgentTimelineHeader(
11369
- roleLabel,
11370
- timeLabel = '',
11371
- createdAt = ''
11580
+ roleLabel
11372
11581
  ) {
11373
11582
  const header = document.createElement('div');
11374
11583
  header.className = 'agent-message-header';
@@ -11378,16 +11587,6 @@ function buildAgentTimelineHeader(
11378
11587
  role.textContent = roleLabel;
11379
11588
  header.appendChild(role);
11380
11589
 
11381
- if (timeLabel) {
11382
- const time = document.createElement('div');
11383
- time.className = 'agent-message-time';
11384
- time.textContent = timeLabel;
11385
- if (createdAt) {
11386
- time.dataset.createdAt = createdAt;
11387
- }
11388
- header.appendChild(time);
11389
- }
11390
-
11391
11590
  return header;
11392
11591
  }
11393
11592
 
@@ -13588,18 +13787,51 @@ function upsertAgentTab(server, data) {
13588
13787
  return agentTab;
13589
13788
  }
13590
13789
 
13591
- function getAgentMessageComparableSignature(message) {
13592
- if (!message || typeof message !== 'object') {
13593
- return '';
13790
+ function buildComparableAgentTimelineTail(source, limit = 6) {
13791
+ const items = [];
13792
+ const messages = Array.isArray(source?.messages) ? source.messages : [];
13793
+ const toolCalls = Array.isArray(source?.toolCalls)
13794
+ ? source.toolCalls
13795
+ : Array.from(source?.toolCalls?.values?.() || []);
13796
+ const permissions = Array.isArray(source?.permissions)
13797
+ ? source.permissions
13798
+ : Array.from(source?.permissions?.values?.() || []);
13799
+ for (const message of messages) {
13800
+ items.push([
13801
+ 'message',
13802
+ Number.isFinite(message?.order) ? message.order : 0,
13803
+ String(message?.id || ''),
13804
+ String(message?.streamKey || ''),
13805
+ String(message?.role || ''),
13806
+ String(message?.kind || ''),
13807
+ hashUiText(message?.text || '')
13808
+ ]);
13594
13809
  }
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
- ]);
13810
+ for (const toolCall of toolCalls) {
13811
+ items.push([
13812
+ 'tool',
13813
+ Number.isFinite(toolCall?.order) ? toolCall.order : 0,
13814
+ String(toolCall?.toolCallId || ''),
13815
+ String(toolCall?.status || ''),
13816
+ hashUiText(JSON.stringify(toolCall || null))
13817
+ ]);
13818
+ }
13819
+ for (const permission of permissions) {
13820
+ items.push([
13821
+ 'permission',
13822
+ Number.isFinite(permission?.order) ? permission.order : 0,
13823
+ String(permission?.id || ''),
13824
+ String(permission?.status || ''),
13825
+ String(permission?.selectedOptionId || '')
13826
+ ]);
13827
+ }
13828
+ items.sort((left, right) => {
13829
+ if (left[1] !== right[1]) {
13830
+ return left[1] - right[1];
13831
+ }
13832
+ return String(left[0]).localeCompare(String(right[0]));
13833
+ });
13834
+ return JSON.stringify(items.slice(-limit));
13603
13835
  }
13604
13836
 
13605
13837
  function shouldApplyAuthoritativeAgentSnapshot(existing, data) {
@@ -13613,16 +13845,15 @@ function shouldApplyAuthoritativeAgentSnapshot(existing, data) {
13613
13845
  if (!Array.isArray(data.messages)) {
13614
13846
  return true;
13615
13847
  }
13616
- const currentMessages = Array.isArray(existing.messages)
13617
- ? existing.messages
13618
- : [];
13619
- if (data.messages.length !== currentMessages.length) {
13848
+ if (
13849
+ Array.isArray(data.messages)
13850
+ && Array.isArray(existing.messages)
13851
+ && data.messages.length !== existing.messages.length
13852
+ ) {
13620
13853
  return true;
13621
13854
  }
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);
13855
+ return buildComparableAgentTimelineTail(data)
13856
+ !== buildComparableAgentTimelineTail(existing);
13626
13857
  }
13627
13858
 
13628
13859
  function upsertAgentInventoryTab(server, data) {
@@ -13719,6 +13950,7 @@ function removeAgentTab(agentTabKey) {
13719
13950
  const agentTab = state.agentTabs.get(agentTabKey);
13720
13951
  if (!agentTab) return;
13721
13952
  const session = agentTab.getLinkedSession();
13953
+ editorManager?.clearScheduledAgentPanelRender?.(agentTabKey);
13722
13954
  agentTab.dispose();
13723
13955
  state.agentTabs.delete(agentTabKey);
13724
13956
 
@@ -319,6 +319,28 @@ function hasGhCopilotCliInstalled() {
319
319
  return ghCopilotCliInstalledCache;
320
320
  }
321
321
 
322
+ function getCodexAcpPlatformPackage() {
323
+ const suffixByPlatform = {
324
+ darwin: {
325
+ arm64: 'darwin-arm64',
326
+ x64: 'darwin-x64'
327
+ },
328
+ linux: {
329
+ arm64: 'linux-arm64',
330
+ x64: 'linux-x64'
331
+ },
332
+ win32: {
333
+ arm64: 'win32-arm64',
334
+ x64: 'win32-x64'
335
+ }
336
+ };
337
+ const platformSuffix = suffixByPlatform[process.platform]?.[process.arch];
338
+ if (!platformSuffix) {
339
+ return '';
340
+ }
341
+ return `@zed-industries/codex-acp-${platformSuffix}`;
342
+ }
343
+
322
344
  function readGhAuthToken() {
323
345
  if (typeof ghAuthTokenCache === 'string') {
324
346
  return ghAuthTokenCache;
@@ -479,6 +501,10 @@ function makeBuiltInDefinitions() {
479
501
  const hasGeminiBinary = commandExists('gemini');
480
502
  const hasCopilotBinary = commandExists('copilot');
481
503
  const hasGhCopilot = hasGhCopilotWrapper();
504
+ const codexAcpPlatformPackage = getCodexAcpPlatformPackage();
505
+ const codexAcpPlatformBinary = codexAcpPlatformPackage
506
+ ? codexAcpPlatformPackage.split('/').pop()
507
+ : '';
482
508
  const definitions = [
483
509
  {
484
510
  id: 'gemini',
@@ -499,8 +525,17 @@ function makeBuiltInDefinitions() {
499
525
  description: 'Codex ACP adapter',
500
526
  websiteUrl: 'https://openai.com/codex/',
501
527
  command: NPX_COMMAND,
502
- args: ['@zed-industries/codex-acp@latest'],
503
- commandLabel: 'npx @zed-industries/codex-acp@latest'
528
+ args: codexAcpPlatformPackage
529
+ ? [
530
+ '-p',
531
+ `${codexAcpPlatformPackage}@latest`,
532
+ codexAcpPlatformBinary
533
+ ]
534
+ : ['@zed-industries/codex-acp@latest'],
535
+ commandLabel: codexAcpPlatformPackage
536
+ ? `npx -p ${codexAcpPlatformPackage}@latest `
537
+ + `${codexAcpPlatformBinary}`
538
+ : 'npx @zed-industries/codex-acp@latest'
504
539
  },
505
540
  {
506
541
  id: 'claude',
@@ -1312,8 +1347,18 @@ export function createRestoreCaptureState(messages = [], options = {}) {
1312
1347
  const toolCalls = Array.isArray(options.toolCalls)
1313
1348
  ? options.toolCalls
1314
1349
  : [];
1350
+ const baselineMessages = normalizeAgentTranscriptMessages(messages);
1351
+ const baselineMessagesByStreamKey = new Map();
1352
+ for (const message of baselineMessages) {
1353
+ const streamKey = String(message?.streamKey || '').trim();
1354
+ if (!streamKey) {
1355
+ continue;
1356
+ }
1357
+ baselineMessagesByStreamKey.set(streamKey, message);
1358
+ }
1315
1359
  return {
1316
- baselineMessages: normalizeAgentTranscriptMessages(messages),
1360
+ baselineMessages,
1361
+ baselineMessagesByStreamKey,
1317
1362
  baselineToolCalls: new Map(
1318
1363
  toolCalls
1319
1364
  .map((entry) => normalizePersistedTimelineEntry(entry, 0))
@@ -1419,6 +1464,44 @@ function getRestoreCaptureStreamKey(capture, update, role, kind, text = '') {
1419
1464
  return streamKey;
1420
1465
  }
1421
1466
 
1467
+ function findMessageIndexByStreamKey(
1468
+ messages = [],
1469
+ streamKey = '',
1470
+ role = '',
1471
+ kind = '',
1472
+ options = {}
1473
+ ) {
1474
+ const normalizedStreamKey = String(streamKey || '').trim();
1475
+ if (!normalizedStreamKey) {
1476
+ return -1;
1477
+ }
1478
+ if (!options.searchWholeHistory) {
1479
+ const lastIndex = messages.length - 1;
1480
+ if (lastIndex < 0) {
1481
+ return -1;
1482
+ }
1483
+ const lastMessage = messages[lastIndex];
1484
+ return (
1485
+ String(lastMessage?.streamKey || '') === normalizedStreamKey
1486
+ && String(lastMessage?.role || '') === String(role || '')
1487
+ && String(lastMessage?.kind || '') === String(kind || '')
1488
+ )
1489
+ ? lastIndex
1490
+ : -1;
1491
+ }
1492
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1493
+ const message = messages[index];
1494
+ if (
1495
+ String(message?.streamKey || '') === normalizedStreamKey
1496
+ && String(message?.role || '') === String(role || '')
1497
+ && String(message?.kind || '') === String(kind || '')
1498
+ ) {
1499
+ return index;
1500
+ }
1501
+ }
1502
+ return -1;
1503
+ }
1504
+
1422
1505
  export function captureRestoreReplayChunk(capture, update, role, kind, text) {
1423
1506
  if (!capture) return false;
1424
1507
  const chunk = String(text || '');
@@ -1430,20 +1513,29 @@ export function captureRestoreReplayChunk(capture, update, role, kind, text) {
1430
1513
  kind,
1431
1514
  chunk
1432
1515
  );
1433
- const last = capture.messages[capture.messages.length - 1] || null;
1434
- if (
1435
- last
1436
- && last.streamKey === streamKey
1437
- && last.role === role
1438
- && last.kind === kind
1439
- ) {
1440
- last.text = mergeAgentMessageText(last.text, chunk);
1516
+ const existingIndex = findMessageIndexByStreamKey(
1517
+ capture.messages,
1518
+ streamKey,
1519
+ role,
1520
+ kind,
1521
+ {
1522
+ searchWholeHistory: !!update?.messageId
1523
+ }
1524
+ );
1525
+ if (existingIndex !== -1) {
1526
+ const existing = capture.messages[existingIndex];
1527
+ existing.text = mergeAgentMessageText(existing.text, chunk);
1528
+ if (typeof capture.nextTimelineOrder === 'function') {
1529
+ existing.order = capture.nextTimelineOrder();
1530
+ }
1441
1531
  return true;
1442
1532
  }
1443
1533
  if (!update?.messageId) {
1444
1534
  capture.messageCounter += 1;
1445
1535
  }
1446
- const baselineMessage = capture.baselineMessages[capture.messages.length] || null;
1536
+ const baselineMessage = capture.baselineMessagesByStreamKey.get(streamKey)
1537
+ || capture.baselineMessages[capture.messages.length]
1538
+ || null;
1447
1539
  const canReuseBaseline = !!(
1448
1540
  baselineMessage
1449
1541
  && baselineMessage.role === role
@@ -1535,18 +1627,32 @@ export function buildRestoredToolCall(
1535
1627
  const persisted = cloneSerializable(baseline, {}) || {};
1536
1628
  const current = cloneSerializable(previous, {}) || {};
1537
1629
  const allowEmptyCreatedAt = options.allowEmptyCreatedAt === true;
1538
- const nextOrder = normalizePersistedTimelineOrder(current.order, 0)
1539
- || (
1540
- typeof nextTimelineOrder === 'function'
1541
- ? nextTimelineOrder()
1542
- : normalizePersistedTimelineOrder(persisted.order, 0)
1543
- )
1630
+ const hasCurrentCreatedAt = Object.prototype.hasOwnProperty.call(
1631
+ current,
1632
+ 'createdAt'
1633
+ );
1634
+ const hasPersistedCreatedAt = Object.prototype.hasOwnProperty.call(
1635
+ persisted,
1636
+ 'createdAt'
1637
+ );
1638
+ const nextOrder = (
1639
+ typeof nextTimelineOrder === 'function'
1640
+ ? nextTimelineOrder()
1641
+ : normalizePersistedTimelineOrder(current.order, 0)
1642
+ )
1643
+ || normalizePersistedTimelineOrder(persisted.order, 0)
1544
1644
  || 1;
1545
1645
  const inheritedCreatedAt = String(
1546
1646
  current.createdAt || persisted.createdAt || ''
1547
1647
  ).trim();
1548
1648
  const createdAt = inheritedCreatedAt
1549
- || (allowEmptyCreatedAt ? '' : new Date().toISOString());
1649
+ || (
1650
+ allowEmptyCreatedAt
1651
+ || hasCurrentCreatedAt
1652
+ || hasPersistedCreatedAt
1653
+ ? ''
1654
+ : new Date().toISOString()
1655
+ );
1550
1656
  const nextToolCall = {
1551
1657
  ...persisted,
1552
1658
  ...current,
@@ -1646,6 +1752,13 @@ function normalizePersistedTerminalSummary(summary = {}) {
1646
1752
  };
1647
1753
  }
1648
1754
 
1755
+ function compareTimelineOrder(left = {}, right = {}) {
1756
+ return (
1757
+ normalizePersistedTimelineOrder(left?.order, 0)
1758
+ - normalizePersistedTimelineOrder(right?.order, 0)
1759
+ );
1760
+ }
1761
+
1649
1762
  export function getNextSyntheticStreamTurn(messages = []) {
1650
1763
  if (!Array.isArray(messages) || messages.length === 0) {
1651
1764
  return 0;
@@ -2659,6 +2772,27 @@ class AcpRuntime extends EventEmitter {
2659
2772
 
2660
2773
  serializeTab(tab) {
2661
2774
  tab.messages = normalizeAgentTranscriptMessages(tab.messages);
2775
+ const messages = cloneSerializable(tab.messages, []).sort(
2776
+ compareTimelineOrder
2777
+ );
2778
+ const toolCalls = Array.from(tab.toolCalls.values())
2779
+ .map((item) => cloneSerializable(item, {}))
2780
+ .sort(compareTimelineOrder);
2781
+ const permissions = Array.from(tab.permissions.values())
2782
+ .map((item) => ({
2783
+ id: item.id,
2784
+ sessionId: item.sessionId,
2785
+ toolCall: item.toolCall,
2786
+ options: item.options,
2787
+ status: item.status,
2788
+ createdAt: item.createdAt || '',
2789
+ order: item.order,
2790
+ selectedOptionId: item.selectedOptionId || ''
2791
+ }))
2792
+ .sort(compareTimelineOrder);
2793
+ const plan = Array.isArray(tab.plan)
2794
+ ? cloneSerializable(tab.plan, []).sort(compareTimelineOrder)
2795
+ : [];
2662
2796
  return {
2663
2797
  id: tab.id,
2664
2798
  runtimeId: tab.runtimeId,
@@ -2679,19 +2813,10 @@ class AcpRuntime extends EventEmitter {
2679
2813
  availableCommands: tab.availableCommands,
2680
2814
  sessionCapabilities: this.#getSessionCapabilities(),
2681
2815
  configOptions: tab.configOptions,
2682
- messages: tab.messages,
2683
- toolCalls: Array.from(tab.toolCalls.values()),
2684
- permissions: Array.from(tab.permissions.values()).map((item) => ({
2685
- id: item.id,
2686
- sessionId: item.sessionId,
2687
- toolCall: item.toolCall,
2688
- options: item.options,
2689
- status: item.status,
2690
- createdAt: item.createdAt || '',
2691
- order: item.order,
2692
- selectedOptionId: item.selectedOptionId || ''
2693
- })),
2694
- plan: Array.isArray(tab.plan) ? tab.plan : [],
2816
+ messages,
2817
+ toolCalls,
2818
+ permissions,
2819
+ plan,
2695
2820
  usage: serializeUsageState(tab.usage),
2696
2821
  terminals: Array.from(tab.terminals.values())
2697
2822
  };
@@ -3257,23 +3382,30 @@ class AcpRuntime extends EventEmitter {
3257
3382
  };
3258
3383
  }
3259
3384
  const streamKey = this.#getStreamKey(tab, update, role, kind);
3260
- const last = tab.messages[tab.messages.length - 1] || null;
3385
+ const existingIndex = findMessageIndexByStreamKey(
3386
+ tab.messages,
3387
+ streamKey,
3388
+ role,
3389
+ kind,
3390
+ {
3391
+ searchWholeHistory: !!update?.messageId
3392
+ }
3393
+ );
3261
3394
 
3262
- if (
3263
- last
3264
- && last.streamKey === streamKey
3265
- && last.role === role
3266
- && last.kind === kind
3267
- ) {
3268
- const nextText = mergeAgentMessageText(last.text, text);
3269
- const appendedText = nextText.slice(last.text.length);
3270
- last.text = nextText;
3395
+ if (existingIndex !== -1) {
3396
+ const existing = tab.messages[existingIndex];
3397
+ const nextText = mergeAgentMessageText(existing.text, text);
3398
+ const appendedText = nextText.slice(existing.text.length);
3399
+ const nextOrder = this.#nextTimelineOrder(tab);
3400
+ existing.text = nextText;
3401
+ existing.order = nextOrder;
3271
3402
  this.#broadcast(tab, {
3272
3403
  type: 'message_chunk',
3273
3404
  streamKey,
3274
3405
  role,
3275
3406
  kind,
3276
- text: appendedText
3407
+ text: appendedText,
3408
+ order: nextOrder
3277
3409
  });
3278
3410
  return {
3279
3411
  didChange: true,
@@ -581,6 +581,53 @@ class TabminalTestAgent {
581
581
  return { stopReason: 'end_turn' };
582
582
  }
583
583
 
584
+ if (
585
+ commandName === 'inline-order'
586
+ || /synthetic-inline-order/i.test(promptText)
587
+ ) {
588
+ await this.connection.sessionUpdate({
589
+ sessionId: params.sessionId,
590
+ update: {
591
+ sessionUpdate: 'agent_message_chunk',
592
+ messageId: 'inline-order-message',
593
+ content: {
594
+ type: 'text',
595
+ text: 'Before tool. '
596
+ }
597
+ }
598
+ });
599
+ await this.connection.sessionUpdate({
600
+ sessionId: params.sessionId,
601
+ update: {
602
+ sessionUpdate: 'tool_call',
603
+ toolCallId: 'inline-order-tool',
604
+ title: 'Inline order tool',
605
+ kind: 'execute',
606
+ status: 'pending'
607
+ }
608
+ });
609
+ await this.connection.sessionUpdate({
610
+ sessionId: params.sessionId,
611
+ update: {
612
+ sessionUpdate: 'tool_call_update',
613
+ toolCallId: 'inline-order-tool',
614
+ status: 'completed'
615
+ }
616
+ });
617
+ await this.connection.sessionUpdate({
618
+ sessionId: params.sessionId,
619
+ update: {
620
+ sessionUpdate: 'agent_message_chunk',
621
+ messageId: 'inline-order-message',
622
+ content: {
623
+ type: 'text',
624
+ text: 'After tool.'
625
+ }
626
+ }
627
+ });
628
+ return { stopReason: 'end_turn' };
629
+ }
630
+
584
631
  if (
585
632
  commandName === 'demo'
586
633
  || commandName === 'diff'