tabminal 3.0.14 → 3.0.15

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.14",
3
+ "version": "3.0.15",
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
@@ -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,64 @@ 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
+ const session = this.currentSession;
4268
+ if (!session) {
4269
+ return;
4270
+ }
4271
+
4272
+ const renderToken = `${Date.now()}:${Math.random()}`;
4273
+ body.dataset.markdownRenderToken = renderToken;
4274
+
4275
+ try {
4276
+ const { renderer } = await loadMarkdownPreviewBundle();
4277
+ if (
4278
+ !body.isConnected
4279
+ || body.dataset.markdownRenderToken !== renderToken
4280
+ ) {
4281
+ return;
4282
+ }
4283
+ const rendered = renderer.render(String(message.text || ''));
4284
+ const sanitized = DOMPurify.sanitize(rendered, {
4285
+ USE_PROFILES: {
4286
+ html: true,
4287
+ mathMl: true,
4288
+ svg: true
4289
+ }
4290
+ });
4291
+ const template = document.createElement('template');
4292
+ template.innerHTML = sanitized;
4293
+ const basePath = buildMarkdownContextBasePath(
4294
+ '',
4295
+ this.getAgentMarkdownBaseDirectory(agentTab, message)
4296
+ );
4297
+ this.decorateMarkdownPreviewContent(
4298
+ template.content,
4299
+ basePath,
4300
+ session
4301
+ );
4302
+ body.replaceChildren(template.content);
4303
+ } catch {
4304
+ // Keep the lightweight fallback rendering.
4305
+ }
4306
+ }
4307
+
4200
4308
  scrollMarkdownPreviewHash(hash) {
4201
4309
  const nextHash = String(hash || '').trim();
4202
4310
  if (!nextHash || !this.markdownPreviewScroll) {
@@ -5171,7 +5279,10 @@ class EditorManager {
5171
5279
  );
5172
5280
 
5173
5281
  const label = document.createElement('span');
5174
- label.textContent = getAgentDisplayLabel(agentTab);
5282
+ tab.title = String(getAgentDisplayLabel(agentTab) || '').trim();
5283
+ label.textContent = formatWorkspaceTabTitle(
5284
+ getAgentDisplayLabel(agentTab)
5285
+ );
5175
5286
 
5176
5287
  const closeBtn = document.createElement('span');
5177
5288
  closeBtn.className = 'close-btn';
@@ -5475,7 +5586,9 @@ class EditorManager {
5475
5586
  this.hideMarkdownPreview();
5476
5587
  this.emptyState.style.display = 'none';
5477
5588
  this.agentContainer.style.display = 'flex';
5478
- this.renderAgentPanel(agentTab);
5589
+ this.renderAgentPanel(agentTab, {
5590
+ reason: isRestore ? 'activate-restore' : 'activate'
5591
+ });
5479
5592
  }
5480
5593
 
5481
5594
  async closeAgentTab(agentTabKey) {
@@ -5485,7 +5598,7 @@ class EditorManager {
5485
5598
  removeAgentTab(agentTabKey);
5486
5599
  }
5487
5600
 
5488
- renderAgentPanel(agentTab) {
5601
+ renderAgentPanel(agentTab, options = {}) {
5489
5602
  this.disposeAgentEmbeddedEditors();
5490
5603
  const previousLayout = this.captureAgentTranscriptLayout();
5491
5604
  const previousScrollTop = previousLayout?.scrollTop || 0;
@@ -5552,12 +5665,29 @@ class EditorManager {
5552
5665
 
5553
5666
  this.agentTranscript.innerHTML = '';
5554
5667
  const timeline = getAgentTimelineItems(agentTab);
5668
+ const shouldPinToBottom = wasNearBottom || (
5669
+ agentTab.scrollToBottomOnNextRender
5670
+ && isAgentTranscriptWindowNearLatest(
5671
+ agentTab,
5672
+ timeline.length
5673
+ )
5674
+ );
5675
+ const transcriptWindow = getAgentTranscriptWindow(
5676
+ agentTab,
5677
+ timeline.length,
5678
+ { pinToBottom: shouldPinToBottom }
5679
+ );
5680
+ const visibleTimeline = timeline.slice(
5681
+ transcriptWindow.start,
5682
+ transcriptWindow.end
5683
+ );
5555
5684
  if (timeline.length === 0) {
5556
5685
  this.agentTranscript.appendChild(
5557
5686
  this.buildAgentEmptyState(agentTab)
5558
5687
  );
5559
5688
  } else {
5560
- for (const [index, entry] of timeline.entries()) {
5689
+ for (const [index, entry] of visibleTimeline.entries()) {
5690
+ const timelineIndex = transcriptWindow.start + index;
5561
5691
  let node = null;
5562
5692
  if (entry.type === 'message') {
5563
5693
  node = this.buildAgentMessageNode(agentTab, entry.value);
@@ -5575,8 +5705,12 @@ class EditorManager {
5575
5705
  );
5576
5706
  }
5577
5707
  if (node) {
5708
+ node.dataset.timelineKey = getAgentTimelineItemKey(
5709
+ entry,
5710
+ timelineIndex
5711
+ );
5578
5712
  if (
5579
- index > 0
5713
+ timelineIndex > 0
5580
5714
  && entry.type === 'message'
5581
5715
  && String(entry.value?.role || '').toLowerCase()
5582
5716
  === 'user'
@@ -5587,13 +5721,19 @@ class EditorManager {
5587
5721
  }
5588
5722
  }
5589
5723
  }
5590
- const shouldPinToBottom = agentTab.scrollToBottomOnNextRender
5591
- || wasNearBottom;
5592
- if (shouldPinToBottom) {
5724
+ if (options.preserveTranscriptAnchor) {
5725
+ const restored = this.restoreAgentTranscriptAnchor(
5726
+ options.preserveTranscriptAnchor
5727
+ );
5728
+ if (!restored) {
5729
+ this.agentTranscript.scrollTop = previousScrollTop;
5730
+ }
5731
+ } else if (shouldPinToBottom) {
5593
5732
  this.agentTranscript.scrollTop = this.agentTranscript.scrollHeight;
5594
5733
  agentTab.scrollToBottomOnNextRender = false;
5595
5734
  } else {
5596
5735
  this.agentTranscript.scrollTop = previousScrollTop;
5736
+ agentTab.scrollToBottomOnNextRender = false;
5597
5737
  }
5598
5738
  this.updateAgentScrollBottomButton();
5599
5739
  this.rememberAgentTranscriptLayout();
@@ -5611,6 +5751,95 @@ class EditorManager {
5611
5751
  this.scheduleAgentTranscriptViewportUpdate(shouldPinToBottom);
5612
5752
  }
5613
5753
 
5754
+ async loadOlderAgentTimeline(agentTab) {
5755
+ if (!agentTab || agentTab.historyWindowLoading) {
5756
+ return;
5757
+ }
5758
+ const timeline = getAgentTimelineItems(agentTab);
5759
+ const transcriptWindow = getAgentTranscriptWindow(
5760
+ agentTab,
5761
+ timeline.length
5762
+ );
5763
+ if (transcriptWindow.start <= 0) {
5764
+ return;
5765
+ }
5766
+ agentTab.scrollToBottomOnNextRender = false;
5767
+ const currentWindowSize = transcriptWindow.end
5768
+ - transcriptWindow.start;
5769
+ const step = Math.min(
5770
+ AGENT_TRANSCRIPT_WINDOW_STEP,
5771
+ transcriptWindow.start
5772
+ );
5773
+ const nextStart = Math.max(0, transcriptWindow.start - step);
5774
+ const anchor = this.captureAgentTranscriptAnchor(
5775
+ getAgentTimelineItemKey(
5776
+ timeline[transcriptWindow.start],
5777
+ transcriptWindow.start
5778
+ )
5779
+ );
5780
+ agentTab.historyWindowLoading = true;
5781
+ agentTab.historyWindowStart = nextStart;
5782
+ agentTab.historyWindowEnd = Math.min(
5783
+ timeline.length,
5784
+ nextStart + currentWindowSize
5785
+ );
5786
+ try {
5787
+ this.renderAgentPanel(agentTab, {
5788
+ reason: 'history-older',
5789
+ preserveTranscriptAnchor: anchor
5790
+ });
5791
+ } finally {
5792
+ agentTab.historyWindowLoading = false;
5793
+ }
5794
+ }
5795
+
5796
+ async loadNewerAgentTimeline(agentTab) {
5797
+ if (!agentTab || agentTab.historyWindowLoading) {
5798
+ return;
5799
+ }
5800
+ const timeline = getAgentTimelineItems(agentTab);
5801
+ const transcriptWindow = getAgentTranscriptWindow(
5802
+ agentTab,
5803
+ timeline.length
5804
+ );
5805
+ if (transcriptWindow.end >= timeline.length) {
5806
+ return;
5807
+ }
5808
+ agentTab.scrollToBottomOnNextRender = false;
5809
+ const currentWindowSize = transcriptWindow.end
5810
+ - transcriptWindow.start;
5811
+ const step = Math.min(
5812
+ AGENT_TRANSCRIPT_WINDOW_STEP,
5813
+ timeline.length - transcriptWindow.end
5814
+ );
5815
+ const nextEnd = Math.min(
5816
+ timeline.length,
5817
+ transcriptWindow.end + step
5818
+ );
5819
+ const nextStart = Math.max(0, nextEnd - currentWindowSize);
5820
+ const anchorIndex = Math.max(
5821
+ transcriptWindow.start,
5822
+ transcriptWindow.end - 1
5823
+ );
5824
+ const anchor = this.captureAgentTranscriptAnchor(
5825
+ getAgentTimelineItemKey(
5826
+ timeline[anchorIndex],
5827
+ anchorIndex
5828
+ )
5829
+ );
5830
+ agentTab.historyWindowLoading = true;
5831
+ agentTab.historyWindowStart = nextStart;
5832
+ agentTab.historyWindowEnd = nextEnd;
5833
+ try {
5834
+ this.renderAgentPanel(agentTab, {
5835
+ reason: 'history-newer',
5836
+ preserveTranscriptAnchor: anchor
5837
+ });
5838
+ } finally {
5839
+ agentTab.historyWindowLoading = false;
5840
+ }
5841
+ }
5842
+
5614
5843
  refreshAgentTimelineTimestamps() {
5615
5844
  if (!this.agentContainer || this.agentContainer.style.display === 'none') {
5616
5845
  return;
@@ -5914,6 +6143,7 @@ class EditorManager {
5914
6143
  ) {
5915
6144
  body.classList.add('markdown');
5916
6145
  body.innerHTML = renderAgentMessageMarkdown(message.text || '');
6146
+ void this.enhanceAgentMarkdownBody(agentTab, message, body);
5917
6147
  } else {
5918
6148
  body.classList.add('plain');
5919
6149
  body.textContent = message.text || '';
@@ -5999,9 +6229,27 @@ class EditorManager {
5999
6229
  toolStatusClass
6000
6230
  );
6001
6231
  details.appendChild(summary);
6002
- details.appendChild(
6003
- this.buildAgentSectionBody(details, section)
6004
- );
6232
+ const bodyHost = document.createElement('div');
6233
+ bodyHost.className = 'agent-tool-call-section-content';
6234
+ const mountBody = () => {
6235
+ if (bodyHost.dataset.mounted === 'true') {
6236
+ return;
6237
+ }
6238
+ bodyHost.dataset.mounted = 'true';
6239
+ bodyHost.appendChild(
6240
+ this.buildAgentSectionBody(details, section)
6241
+ );
6242
+ };
6243
+ details.appendChild(bodyHost);
6244
+ if (details.open) {
6245
+ queueMicrotask(mountBody);
6246
+ } else {
6247
+ details.addEventListener('toggle', () => {
6248
+ if (details.open) {
6249
+ mountBody();
6250
+ }
6251
+ }, { once: true });
6252
+ }
6005
6253
  sectionContainer.appendChild(details);
6006
6254
  }
6007
6255
  node.appendChild(sectionContainer);
@@ -6093,9 +6341,27 @@ class EditorManager {
6093
6341
  permission.status || 'pending'
6094
6342
  );
6095
6343
  details.appendChild(summary);
6096
- details.appendChild(
6097
- this.buildAgentSectionBody(details, section)
6098
- );
6344
+ const bodyHost = document.createElement('div');
6345
+ bodyHost.className = 'agent-tool-call-section-content';
6346
+ const mountBody = () => {
6347
+ if (bodyHost.dataset.mounted === 'true') {
6348
+ return;
6349
+ }
6350
+ bodyHost.dataset.mounted = 'true';
6351
+ bodyHost.appendChild(
6352
+ this.buildAgentSectionBody(details, section)
6353
+ );
6354
+ };
6355
+ details.appendChild(bodyHost);
6356
+ if (details.open) {
6357
+ queueMicrotask(mountBody);
6358
+ } else {
6359
+ details.addEventListener('toggle', () => {
6360
+ if (details.open) {
6361
+ mountBody();
6362
+ }
6363
+ }, { once: true });
6364
+ }
6099
6365
  sectionContainer.appendChild(details);
6100
6366
  }
6101
6367
  card.appendChild(sectionContainer);
@@ -6790,6 +7056,51 @@ class EditorManager {
6790
7056
  };
6791
7057
  }
6792
7058
 
7059
+ findAgentTranscriptNodeByKey(timelineKey = '') {
7060
+ if (!this.agentTranscript || !timelineKey) {
7061
+ return null;
7062
+ }
7063
+ for (const node of this.agentTranscript.children) {
7064
+ if (node?.dataset?.timelineKey === timelineKey) {
7065
+ return node;
7066
+ }
7067
+ }
7068
+ return null;
7069
+ }
7070
+
7071
+ captureAgentTranscriptAnchor(timelineKey = '') {
7072
+ const node = this.findAgentTranscriptNodeByKey(timelineKey);
7073
+ if (!node || !this.agentTranscript) {
7074
+ return null;
7075
+ }
7076
+ return {
7077
+ timelineKey,
7078
+ scrollTop: this.agentTranscript.scrollTop,
7079
+ offsetTop: node.offsetTop
7080
+ };
7081
+ }
7082
+
7083
+ restoreAgentTranscriptAnchor(anchor = null) {
7084
+ if (!anchor || !this.agentTranscript) {
7085
+ return false;
7086
+ }
7087
+ const node = this.findAgentTranscriptNodeByKey(
7088
+ anchor.timelineKey || ''
7089
+ );
7090
+ if (!node) {
7091
+ return false;
7092
+ }
7093
+ const previousOffsetTop = Number.isFinite(anchor.offsetTop)
7094
+ ? anchor.offsetTop
7095
+ : 0;
7096
+ const previousScrollTop = Number.isFinite(anchor.scrollTop)
7097
+ ? anchor.scrollTop
7098
+ : 0;
7099
+ this.agentTranscript.scrollTop = previousScrollTop
7100
+ + (node.offsetTop - previousOffsetTop);
7101
+ return true;
7102
+ }
7103
+
6793
7104
  rememberAgentTranscriptLayout() {
6794
7105
  this.agentTranscriptLayout = this.captureAgentTranscriptLayout();
6795
7106
  }
@@ -6810,6 +7121,32 @@ class EditorManager {
6810
7121
  }
6811
7122
 
6812
7123
  scrollAgentTranscriptToBottom() {
7124
+ const activeTab = getActiveAgentTab();
7125
+ if (activeTab) {
7126
+ const total = getAgentTimelineItems(activeTab).length;
7127
+ const transcriptWindow = getAgentTranscriptWindow(
7128
+ activeTab,
7129
+ total,
7130
+ { pinToBottom: false }
7131
+ );
7132
+ const latestWindow = getAgentTranscriptWindow(
7133
+ null,
7134
+ total,
7135
+ { pinToBottom: true }
7136
+ );
7137
+ const alreadyLatest = transcriptWindow.start === latestWindow.start
7138
+ && transcriptWindow.end === latestWindow.end;
7139
+ if (!alreadyLatest) {
7140
+ activeTab.historyWindowStart = latestWindow.start;
7141
+ activeTab.historyWindowEnd = latestWindow.end;
7142
+ activeTab.scrollToBottomOnNextRender = true;
7143
+ this.renderAgentPanel(activeTab, {
7144
+ reason: 'scroll-latest'
7145
+ });
7146
+ return;
7147
+ }
7148
+ activeTab.scrollToBottomOnNextRender = false;
7149
+ }
6813
7150
  if (!this.agentTranscript) return;
6814
7151
  this.agentTranscript.scrollTop = this.agentTranscript.scrollHeight;
6815
7152
  this.updateAgentScrollBottomButton();
@@ -8315,10 +8652,11 @@ class Session {
8315
8652
 
8316
8653
  const titleEl = tab.querySelector('.title');
8317
8654
  const titleTextEl = tab.querySelector('.tab-title-text');
8655
+ const displayTitle = formatWorkspaceTabTitle(this.title);
8318
8656
  if (titleTextEl) {
8319
- titleTextEl.textContent = this.title;
8657
+ titleTextEl.textContent = displayTitle;
8320
8658
  } else if (titleEl) {
8321
- titleEl.textContent = this.title;
8659
+ titleEl.textContent = displayTitle;
8322
8660
  }
8323
8661
 
8324
8662
  const titleIconEl = tab.querySelector('.tab-status-icon');
@@ -8747,6 +9085,9 @@ class AgentTab {
8747
9085
  this.scrollToBottomOnNextRender = true;
8748
9086
  this.busySyncTimer = null;
8749
9087
  this.planHistory = [];
9088
+ this.historyWindowStart = -1;
9089
+ this.historyWindowEnd = -1;
9090
+ this.historyWindowLoading = false;
8750
9091
  this.resumeSessions = [];
8751
9092
  this.resumeSessionsLoadedAt = 0;
8752
9093
  this.resumeSessionsPromise = null;
@@ -10880,6 +11221,106 @@ function getAgentTimelineItems(agentTab) {
10880
11221
  return items;
10881
11222
  }
10882
11223
 
11224
+ function getAgentTimelineItemKey(entry, absoluteIndex = 0) {
11225
+ if (!entry) {
11226
+ return `unknown:${absoluteIndex}`;
11227
+ }
11228
+ const order = Number.isFinite(entry.order) ? entry.order : -1;
11229
+ return `${entry.type}:${order}:${absoluteIndex}`;
11230
+ }
11231
+
11232
+ function formatWorkspaceTabTitle(
11233
+ value,
11234
+ maxLength = WORKSPACE_TAB_TITLE_MAX_LENGTH
11235
+ ) {
11236
+ const text = String(value || '');
11237
+ const characters = Array.from(text);
11238
+ if (characters.length <= maxLength) {
11239
+ return text;
11240
+ }
11241
+ if (maxLength <= 3) {
11242
+ return '.'.repeat(Math.max(0, maxLength));
11243
+ }
11244
+ return `${characters.slice(0, maxLength - 3).join('')}...`;
11245
+ }
11246
+
11247
+ function getAgentTranscriptWindow(
11248
+ agentTab,
11249
+ totalCount = 0,
11250
+ options = {}
11251
+ ) {
11252
+ const total = Number.isFinite(totalCount)
11253
+ ? Math.max(0, totalCount)
11254
+ : 0;
11255
+ const windowSize = Math.min(total, AGENT_TRANSCRIPT_INITIAL_VISIBLE_BLOCKS);
11256
+ const latestStart = Math.max(0, total - windowSize);
11257
+ const latestWindow = {
11258
+ start: latestStart,
11259
+ end: total
11260
+ };
11261
+ if (!agentTab) {
11262
+ return latestWindow;
11263
+ }
11264
+ if (windowSize === 0) {
11265
+ agentTab.historyWindowStart = 0;
11266
+ agentTab.historyWindowEnd = 0;
11267
+ return { start: 0, end: 0 };
11268
+ }
11269
+ if (options.pinToBottom) {
11270
+ agentTab.historyWindowStart = latestWindow.start;
11271
+ agentTab.historyWindowEnd = latestWindow.end;
11272
+ return latestWindow;
11273
+ }
11274
+ let start = Number.isFinite(agentTab.historyWindowStart)
11275
+ ? Math.max(0, Math.floor(agentTab.historyWindowStart))
11276
+ : latestWindow.start;
11277
+ let end = Number.isFinite(agentTab.historyWindowEnd)
11278
+ ? Math.max(start, Math.floor(agentTab.historyWindowEnd))
11279
+ : latestWindow.end;
11280
+ if (end > total) {
11281
+ end = total;
11282
+ }
11283
+ if (end - start !== windowSize) {
11284
+ if (total <= windowSize) {
11285
+ start = 0;
11286
+ end = total;
11287
+ } else if (end >= total) {
11288
+ end = total;
11289
+ start = latestWindow.start;
11290
+ } else if (start <= 0) {
11291
+ start = 0;
11292
+ end = windowSize;
11293
+ } else {
11294
+ end = Math.min(total, start + windowSize);
11295
+ start = Math.max(0, end - windowSize);
11296
+ }
11297
+ }
11298
+ agentTab.historyWindowStart = start;
11299
+ agentTab.historyWindowEnd = end;
11300
+ return { start, end };
11301
+ }
11302
+
11303
+ function isAgentTranscriptWindowNearLatest(agentTab, totalCount = 0) {
11304
+ const total = Number.isFinite(totalCount)
11305
+ ? Math.max(0, totalCount)
11306
+ : 0;
11307
+ if (!agentTab) {
11308
+ return true;
11309
+ }
11310
+ if (
11311
+ !Number.isFinite(agentTab.historyWindowStart)
11312
+ || !Number.isFinite(agentTab.historyWindowEnd)
11313
+ || agentTab.historyWindowStart < 0
11314
+ || agentTab.historyWindowEnd < 0
11315
+ ) {
11316
+ return true;
11317
+ }
11318
+ return agentTab.historyWindowEnd >= Math.max(
11319
+ 0,
11320
+ total - AGENT_TRANSCRIPT_FOLLOW_LATEST_TOLERANCE
11321
+ );
11322
+ }
11323
+
10883
11324
  function normalizePlanStatusClass(status = '') {
10884
11325
  const value = String(status || '').toLowerCase();
10885
11326
  if (value === 'completed') return 'completed';
package/public/styles.css CHANGED
@@ -2480,6 +2480,7 @@ kbd {
2480
2480
  justify-content: space-between;
2481
2481
  gap: 10px;
2482
2482
  flex-wrap: wrap;
2483
+ min-width: 0;
2483
2484
  }
2484
2485
 
2485
2486
  .agent-tool-call-title,
@@ -2487,12 +2488,18 @@ kbd {
2487
2488
  color: var(--text-highlight);
2488
2489
  font-weight: 600;
2489
2490
  font-size: 12px;
2491
+ min-width: 0;
2492
+ flex: 1 1 auto;
2493
+ overflow-wrap: anywhere;
2494
+ word-break: break-word;
2490
2495
  }
2491
2496
 
2492
2497
  .agent-tool-call-meta {
2493
2498
  color: var(--text-muted);
2494
2499
  font-size: 10px;
2495
2500
  margin-top: 4px;
2501
+ overflow-wrap: anywhere;
2502
+ word-break: break-word;
2496
2503
  }
2497
2504
 
2498
2505
  .agent-path-links {
@@ -2916,10 +2923,28 @@ kbd {
2916
2923
  margin: 0 0 8px;
2917
2924
  }
2918
2925
 
2926
+ .agent-message-body.markdown h1,
2927
+ .agent-message-body.markdown h2,
2928
+ .agent-message-body.markdown h3,
2929
+ .agent-message-body.markdown h4,
2930
+ .agent-message-body.markdown h5,
2931
+ .agent-message-body.markdown h6 {
2932
+ margin: 0 0 10px;
2933
+ color: var(--text-highlight);
2934
+ line-height: 1.35;
2935
+ }
2936
+
2919
2937
  .agent-message-body.markdown ul {
2920
2938
  padding-left: 18px;
2921
2939
  }
2922
2940
 
2941
+ .agent-message-body.markdown blockquote {
2942
+ margin: 0 0 10px;
2943
+ padding: 2px 0 2px 12px;
2944
+ border-left: 3px solid rgba(38, 139, 210, 0.34);
2945
+ color: var(--text-muted);
2946
+ }
2947
+
2923
2948
  .agent-message-body.markdown code {
2924
2949
  font: inherit;
2925
2950
  font-size: 11px;
@@ -2927,6 +2952,8 @@ kbd {
2927
2952
  border: 1px solid rgba(131, 148, 150, 0.18);
2928
2953
  border-radius: 6px;
2929
2954
  padding: 1px 4px;
2955
+ overflow-wrap: anywhere;
2956
+ word-break: break-word;
2930
2957
  }
2931
2958
 
2932
2959
  .agent-message-body.markdown pre {
@@ -2949,6 +2976,52 @@ kbd {
2949
2976
  color: var(--accent-primary);
2950
2977
  text-decoration: underline;
2951
2978
  text-underline-offset: 2px;
2979
+ overflow-wrap: anywhere;
2980
+ word-break: break-word;
2981
+ }
2982
+
2983
+ .agent-message-body.markdown img {
2984
+ display: block;
2985
+ max-width: 100%;
2986
+ height: auto;
2987
+ margin: 0 0 10px;
2988
+ border-radius: 8px;
2989
+ }
2990
+
2991
+ .agent-message-body.markdown table {
2992
+ display: block;
2993
+ width: max-content;
2994
+ max-width: 100%;
2995
+ overflow-x: auto;
2996
+ border-collapse: collapse;
2997
+ margin: 0 0 10px;
2998
+ }
2999
+
3000
+ .agent-message-body.markdown th,
3001
+ .agent-message-body.markdown td {
3002
+ border: 1px solid rgba(131, 148, 150, 0.2);
3003
+ padding: 6px 8px;
3004
+ text-align: left;
3005
+ }
3006
+
3007
+ .agent-message-body.markdown hr {
3008
+ border: 0;
3009
+ border-top: 1px solid rgba(131, 148, 150, 0.18);
3010
+ margin: 10px 0;
3011
+ }
3012
+
3013
+ .agent-message-body.markdown .task-list-item {
3014
+ list-style: none;
3015
+ }
3016
+
3017
+ .agent-message-body.markdown .task-list-item input {
3018
+ margin-right: 8px;
3019
+ }
3020
+
3021
+ .agent-message-body.markdown .katex-display {
3022
+ overflow-x: auto;
3023
+ overflow-y: hidden;
3024
+ margin: 0 0 10px;
2952
3025
  }
2953
3026
 
2954
3027
  .agent-message-attachments {