tabminal 3.0.7 → 3.0.9

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
@@ -108,7 +108,7 @@ const CLOSE_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" stroke="
108
108
  const AGENT_ICON_SVG = '<svg viewBox="0 0 24 24" width="17" height="17" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="10" rx="2"></rect><path d="M9 7V5"></path><path d="M15 7V5"></path><path d="M12 17v2"></path><path d="M5 12H3"></path><path d="M21 12h-2"></path><path d="M9 11h.01"></path><path d="M15 11h.01"></path><path d="M9.5 14c.7.67 1.53 1 2.5 1s1.8-.33 2.5-1"></path></svg>';
109
109
  const TERMINAL_TAB_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m8 10 3 2-3 2"></path><path d="M13 15h4"></path></svg>';
110
110
  const MANAGED_TERMINAL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M7 12h.01"></path><path d="M12 9v6"></path><path d="M9 12h6"></path><path d="M18 8v2"></path><path d="M19 9h-2"></path></svg>';
111
- const BELL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M10 5a2 2 0 1 1 4 0"></path><path d="M5 16a7 7 0 1 0 14 0"></path><path d="M4 16h16"></path><path d="M10 20a2 2 0 0 0 4 0"></path></svg>';
111
+ const BELL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.1" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4.5a4.5 4.5 0 0 0-4.5 4.5v2.4c0 1.2-.41 2.37-1.17 3.3L5 16.5h14l-1.33-1.8a5.66 5.66 0 0 1-1.17-3.3V9A4.5 4.5 0 0 0 12 4.5"></path><path d="M10.25 19a1.75 1.75 0 0 0 3.5 0"></path></svg>';
112
112
  const SPINNER_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"><path d="M12 3a9 9 0 1 0 9 9"></path></svg>';
113
113
  const ATTACH_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 1 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.82-2.82l8.49-8.49"></path></svg>';
114
114
  const CHEVRON_DOWN_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>';
@@ -375,6 +375,11 @@ function persistRuntimeBootId(bootId) {
375
375
  }
376
376
  }
377
377
 
378
+ function getLoadedRuntimeAssetKey() {
379
+ const assetKey = window.__tabminalRuntimeAssetKey;
380
+ return typeof assetKey === 'string' ? assetKey : '';
381
+ }
382
+
378
383
  function handlePrimaryRuntimeVersion(data) {
379
384
  const runtime = data?.runtime;
380
385
  const bootIdRaw = runtime?.bootId;
@@ -382,16 +387,20 @@ function handlePrimaryRuntimeVersion(data) {
382
387
  const bootId = String(bootIdRaw);
383
388
  if (!bootId) return;
384
389
  const storedBootId = readRuntimeBootId();
390
+ const loadedAssetKey = getLoadedRuntimeAssetKey();
391
+ const needsShellReload = loadedAssetKey !== bootId;
385
392
 
386
393
  if (!primaryServerBootId) {
387
394
  primaryServerBootId = bootId;
388
- if (storedBootId === bootId) {
395
+ if (storedBootId === bootId && !needsShellReload) {
389
396
  return;
390
397
  }
391
398
  const persisted = persistRuntimeBootId(bootId);
392
- if (storedBootId && persisted && !runtimeReloadScheduled) {
399
+ if (persisted && needsShellReload && !runtimeReloadScheduled) {
393
400
  runtimeReloadScheduled = true;
394
- console.info('[Runtime] Syncing app shell cache key with server boot id.');
401
+ console.info(
402
+ '[Runtime] Syncing app shell cache key with server boot id.'
403
+ );
395
404
  window.location.reload();
396
405
  }
397
406
  return;
@@ -400,6 +409,13 @@ function handlePrimaryRuntimeVersion(data) {
400
409
  if (storedBootId !== bootId) {
401
410
  persistRuntimeBootId(bootId);
402
411
  }
412
+ if (needsShellReload && !runtimeReloadScheduled) {
413
+ runtimeReloadScheduled = true;
414
+ console.info(
415
+ '[Runtime] Reloading app shell to match server boot id.'
416
+ );
417
+ window.location.reload();
418
+ }
403
419
  return;
404
420
  }
405
421
  if (runtimeReloadScheduled) return;
@@ -703,6 +719,8 @@ class EditorManager {
703
719
  this.agentCommandMenu = null;
704
720
  this.agentCommandSuggestions = [];
705
721
  this.agentCommandIndex = 0;
722
+ this.agentCommandMenuStateKey = '';
723
+ this.agentCommandMenuToken = 0;
706
724
  this.isApplyingAgentPromptState = false;
707
725
  this.suppressAgentCommandMenu = false;
708
726
  this.agentEmbeddedEditors = [];
@@ -1106,6 +1124,14 @@ class EditorManager {
1106
1124
  });
1107
1125
  this.agentPrompt.addEventListener('blur', () => {
1108
1126
  setTimeout(() => {
1127
+ if (
1128
+ document.activeElement?.classList?.contains(
1129
+ 'xterm-helper-textarea'
1130
+ )
1131
+ && this.agentCommandSuggestions.length > 0
1132
+ ) {
1133
+ return;
1134
+ }
1109
1135
  this.hideAgentCommandMenu();
1110
1136
  }, 120);
1111
1137
  });
@@ -1134,14 +1160,24 @@ class EditorManager {
1134
1160
  ? state.agentTabs.get(activeTabKey)
1135
1161
  : null;
1136
1162
 
1163
+ if (
1164
+ agentTab
1165
+ && this.agentCommandSuggestions.length > 0
1166
+ && Number.isInteger(agentTab.promptHistoryIndex)
1167
+ ) {
1168
+ this.exitAgentPromptHistoryBrowsing(agentTab);
1169
+ }
1170
+
1137
1171
  if (this.agentCommandSuggestions.length > 0) {
1138
1172
  if (event.key === 'ArrowDown') {
1139
1173
  event.preventDefault();
1174
+ event.stopImmediatePropagation();
1140
1175
  this.moveAgentCommandSelection(1);
1141
1176
  return;
1142
1177
  }
1143
1178
  if (event.key === 'ArrowUp') {
1144
1179
  event.preventDefault();
1180
+ event.stopImmediatePropagation();
1145
1181
  this.moveAgentCommandSelection(-1);
1146
1182
  return;
1147
1183
  }
@@ -1156,11 +1192,13 @@ class EditorManager {
1156
1192
  )
1157
1193
  ) {
1158
1194
  event.preventDefault();
1159
- this.applyAgentCommandSuggestion();
1195
+ event.stopImmediatePropagation();
1196
+ void this.applyAgentCommandSuggestion();
1160
1197
  return;
1161
1198
  }
1162
1199
  if (event.key === 'Escape') {
1163
1200
  event.preventDefault();
1201
+ event.stopImmediatePropagation();
1164
1202
  this.hideAgentCommandMenu();
1165
1203
  return;
1166
1204
  }
@@ -1556,9 +1594,7 @@ class EditorManager {
1556
1594
  const hasAgentTabs = getAgentTabsForSession(this.currentSession).length > 0;
1557
1595
  const compact = this.hasCompactWorkspaceTabs(this.currentSession);
1558
1596
  const hasTabs = compact || hasOpenFiles || hasAgentTabs;
1559
- const shouldShow = compact
1560
- ? true
1561
- : state.isVisible || hasOpenFiles || hasAgentTabs;
1597
+ const shouldShow = hasTabs;
1562
1598
 
1563
1599
  this.tabsContainer.style.display = hasTabs ? 'flex' : 'none';
1564
1600
  this.pane.style.display = shouldShow ? 'flex' : 'none';
@@ -1644,8 +1680,7 @@ class EditorManager {
1644
1680
  const state = session.editorState;
1645
1681
 
1646
1682
  // Only render tabs and content, file tree is persistent in sidebar
1647
- const shouldShowWorkspace = state.isVisible
1648
- || this.hasVisibleWorkspaceTabs(session);
1683
+ const shouldShowWorkspace = this.hasVisibleWorkspaceTabs(session);
1649
1684
  if (shouldShowWorkspace) {
1650
1685
  if (state.isVisible) {
1651
1686
  this.refreshSessionTree(session);
@@ -1654,8 +1689,6 @@ class EditorManager {
1654
1689
  const activeKey = this.getActiveWorkspaceTabKey(session);
1655
1690
  if (activeKey) {
1656
1691
  this.activateWorkspaceTab(activeKey, true);
1657
- } else {
1658
- this.showEmptyState();
1659
1692
  }
1660
1693
  }
1661
1694
 
@@ -3155,6 +3188,17 @@ class EditorManager {
3155
3188
  const attachments = Array.isArray(agentTab.pendingAttachments)
3156
3189
  ? [...agentTab.pendingAttachments]
3157
3190
  : [];
3191
+ const promptIntent = getAgentPromptIntent(
3192
+ agentTab,
3193
+ this.agentPrompt.value || ''
3194
+ );
3195
+ if (promptIntent.kind === 'resume') {
3196
+ alert('Select a previous session from the /resume menu.', {
3197
+ type: 'warning',
3198
+ title: getAgentBaseName(agentTab)
3199
+ });
3200
+ return;
3201
+ }
3158
3202
  if (!text && attachments.length === 0) {
3159
3203
  if (canAutostartQueuedAgentPrompt(agentTab)) {
3160
3204
  await drainQueuedAgentPrompt(agentTab);
@@ -3329,7 +3373,21 @@ class EditorManager {
3329
3373
  if (this.suppressAgentCommandMenu) {
3330
3374
  this.hideAgentCommandMenu();
3331
3375
  } else {
3332
- this.renderAgentCommandMenu(activeAgentTab);
3376
+ const promptValue = this.agentPrompt?.value || '';
3377
+ const promptIntent = getAgentPromptIntent(
3378
+ activeAgentTab,
3379
+ promptValue
3380
+ );
3381
+ const nextMenuStateKey = [
3382
+ activeAgentTab?.key || '',
3383
+ promptIntent.kind,
3384
+ promptValue
3385
+ ].join('::');
3386
+ const menuVisible = this.agentCommandMenu
3387
+ && this.agentCommandMenu.style.display !== 'none';
3388
+ if (!menuVisible || this.agentCommandMenuStateKey !== nextMenuStateKey) {
3389
+ this.renderAgentCommandMenu(activeAgentTab);
3390
+ }
3333
3391
  }
3334
3392
  if (
3335
3393
  activeAgentTab
@@ -3500,12 +3558,7 @@ class EditorManager {
3500
3558
  this.agentScrollBottomButton.style.display = shouldShow ? '' : 'none';
3501
3559
  }
3502
3560
 
3503
- renderAgentCommandMenu(agentTab = null) {
3504
- if (!this.agentCommandMenu) return;
3505
- const suggestions = getAgentCommandSuggestions(
3506
- agentTab,
3507
- this.agentPrompt?.value || ''
3508
- );
3561
+ #renderAgentCommandSuggestions(suggestions) {
3509
3562
  this.agentCommandSuggestions = suggestions;
3510
3563
  if (suggestions.length === 0) {
3511
3564
  this.hideAgentCommandMenu();
@@ -3527,7 +3580,11 @@ class EditorManager {
3527
3580
  }
3528
3581
  const name = document.createElement('span');
3529
3582
  name.className = 'agent-command-option-name';
3530
- name.textContent = `/${command.name}`;
3583
+ name.textContent = command.kind === 'resume_session'
3584
+ ? command.displayName || command.title || command.sessionId
3585
+ : command.kind === 'info'
3586
+ ? command.label || ''
3587
+ : `/${command.name}`;
3531
3588
  button.appendChild(name);
3532
3589
  if (command.description) {
3533
3590
  const meta = document.createElement('span');
@@ -3535,12 +3592,16 @@ class EditorManager {
3535
3592
  meta.textContent = command.description;
3536
3593
  button.appendChild(meta);
3537
3594
  }
3595
+ if (command.kind === 'info') {
3596
+ button.disabled = true;
3597
+ }
3538
3598
  button.addEventListener('mousedown', (event) => {
3539
3599
  event.preventDefault();
3540
3600
  });
3541
3601
  button.addEventListener('click', () => {
3602
+ if (command.kind === 'info') return;
3542
3603
  this.agentCommandIndex = index;
3543
- this.applyAgentCommandSuggestion();
3604
+ void this.applyAgentCommandSuggestion();
3544
3605
  });
3545
3606
  this.agentCommandMenu.appendChild(button);
3546
3607
  }
@@ -3554,10 +3615,95 @@ class EditorManager {
3554
3615
  }
3555
3616
  }
3556
3617
 
3618
+ async renderAgentCommandMenu(agentTab = null) {
3619
+ if (!this.agentCommandMenu) return;
3620
+ const promptValue = this.agentPrompt?.value || '';
3621
+ const token = this.agentCommandMenuToken + 1;
3622
+ this.agentCommandMenuToken = token;
3623
+ const intent = getAgentPromptIntent(agentTab, promptValue);
3624
+ const menuStateKey = [
3625
+ agentTab?.key || '',
3626
+ intent.kind,
3627
+ promptValue
3628
+ ].join('::');
3629
+
3630
+ if (!agentTab || intent.kind === 'none' || intent.kind === 'other') {
3631
+ this.hideAgentCommandMenu();
3632
+ return;
3633
+ }
3634
+
3635
+ if (Number.isInteger(agentTab.promptHistoryIndex)) {
3636
+ this.exitAgentPromptHistoryBrowsing(agentTab);
3637
+ }
3638
+
3639
+ if (intent.kind === 'resume') {
3640
+ const hasLoadedResumeSuggestions = (
3641
+ this.agentCommandMenuStateKey === menuStateKey
3642
+ && this.agentCommandSuggestions.length > 0
3643
+ && !(
3644
+ this.agentCommandSuggestions.length === 1
3645
+ && this.agentCommandSuggestions[0]?.kind === 'info'
3646
+ && /loading previous sessions/i.test(
3647
+ this.agentCommandSuggestions[0]?.label || ''
3648
+ )
3649
+ )
3650
+ );
3651
+ if (hasLoadedResumeSuggestions) {
3652
+ this.#renderAgentCommandSuggestions(
3653
+ this.agentCommandSuggestions
3654
+ );
3655
+ return;
3656
+ }
3657
+ this.agentCommandMenuStateKey = menuStateKey;
3658
+ this.#renderAgentCommandSuggestions([{
3659
+ kind: 'info',
3660
+ label: 'Loading previous sessions…',
3661
+ description: ''
3662
+ }]);
3663
+ try {
3664
+ const sessions = await agentTab.listResumeSessions();
3665
+ if (this.agentCommandMenuToken !== token) {
3666
+ return;
3667
+ }
3668
+ const suggestions = getAgentResumeSuggestions(
3669
+ agentTab,
3670
+ promptValue,
3671
+ sessions
3672
+ );
3673
+ if (suggestions.length === 0) {
3674
+ this.#renderAgentCommandSuggestions([{
3675
+ kind: 'info',
3676
+ label: 'No previous sessions found',
3677
+ description: ''
3678
+ }]);
3679
+ return;
3680
+ }
3681
+ this.#renderAgentCommandSuggestions(suggestions);
3682
+ } catch (error) {
3683
+ if (this.agentCommandMenuToken !== token) {
3684
+ return;
3685
+ }
3686
+ this.#renderAgentCommandSuggestions([{
3687
+ kind: 'info',
3688
+ label: 'Unable to load previous sessions',
3689
+ description: error?.message || ''
3690
+ }]);
3691
+ }
3692
+ return;
3693
+ }
3694
+
3695
+ this.agentCommandMenuStateKey = menuStateKey;
3696
+ this.#renderAgentCommandSuggestions(
3697
+ getAgentCommandSuggestions(agentTab, promptValue)
3698
+ );
3699
+ }
3700
+
3557
3701
  hideAgentCommandMenu() {
3558
3702
  if (!this.agentCommandMenu) return;
3703
+ this.agentCommandMenuToken += 1;
3559
3704
  this.agentCommandSuggestions = [];
3560
3705
  this.agentCommandIndex = 0;
3706
+ this.agentCommandMenuStateKey = '';
3561
3707
  this.agentCommandMenu.style.display = 'none';
3562
3708
  this.agentCommandMenu.innerHTML = '';
3563
3709
  }
@@ -3568,7 +3714,7 @@ class EditorManager {
3568
3714
  this.agentCommandIndex = nextIndex < 0
3569
3715
  ? this.agentCommandSuggestions.length - 1
3570
3716
  : nextIndex % this.agentCommandSuggestions.length;
3571
- this.renderAgentCommandMenu(getActiveAgentTab());
3717
+ this.#renderAgentCommandSuggestions(this.agentCommandSuggestions);
3572
3718
  }
3573
3719
 
3574
3720
  setAgentPromptValue(value, agentTab = null, options = {}) {
@@ -3739,9 +3885,40 @@ class EditorManager {
3739
3885
  agentTab.promptDraft = this.agentPrompt?.value || '';
3740
3886
  }
3741
3887
 
3742
- applyAgentCommandSuggestion() {
3888
+ async applyAgentCommandSuggestion() {
3743
3889
  const command = this.agentCommandSuggestions[this.agentCommandIndex];
3744
3890
  if (!command) return;
3891
+ if (command.kind === 'info') {
3892
+ return;
3893
+ }
3894
+ if (command.kind === 'resume_session') {
3895
+ const agentTab = getActiveAgentTab();
3896
+ if (!agentTab) return;
3897
+ const session = agentTab.getLinkedSession();
3898
+ if (!session) return;
3899
+ try {
3900
+ if (command.openTabKey) {
3901
+ const existingTab = state.agentTabs.get(command.openTabKey);
3902
+ const existingSession = existingTab?.getLinkedSession() || null;
3903
+ if (existingTab && existingSession) {
3904
+ await activateAgentTab(existingSession, existingTab, {
3905
+ switchSession: true
3906
+ });
3907
+ } else {
3908
+ await resumeAgentTabFromHistory(session, agentTab, command);
3909
+ }
3910
+ } else {
3911
+ await resumeAgentTabFromHistory(session, agentTab, command);
3912
+ }
3913
+ this.hideAgentCommandMenu();
3914
+ } catch (error) {
3915
+ alert(error.message, {
3916
+ type: 'error',
3917
+ title: getAgentBaseName(agentTab)
3918
+ });
3919
+ }
3920
+ return;
3921
+ }
3745
3922
  const suffix = command.inputHint
3746
3923
  ? ` ${command.inputHint}`
3747
3924
  : ' ';
@@ -4464,6 +4641,10 @@ class Session {
4464
4641
  editorFlex: '2 1 0%'
4465
4642
  };
4466
4643
  this.previewRelayoutScheduled = false;
4644
+ this.lastTerminalControlClaimAt = 0;
4645
+ this.boundTerminalClaimRoot = null;
4646
+ this.boundTerminalClaimTextarea = null;
4647
+ this.boundTerminalClaimHandler = null;
4467
4648
  this.wrapperElement = null;
4468
4649
  this._createTerminals();
4469
4650
 
@@ -4529,6 +4710,8 @@ class Session {
4529
4710
  const wasActive = state.activeSessionKey === this.key;
4530
4711
  const previewWrapper = this.wrapperElement;
4531
4712
 
4713
+ this.unbindTerminalControlClaim();
4714
+
4532
4715
  try {
4533
4716
  this.previewTerm?.dispose();
4534
4717
  } catch (e) {
@@ -4556,6 +4739,7 @@ class Session {
4556
4739
  if (wasActive && terminalEl) {
4557
4740
  terminalEl.innerHTML = '';
4558
4741
  this.mainTerm.open(terminalEl);
4742
+ this.bindTerminalControlClaim();
4559
4743
  if (this.fitMainTerminalIfVisible()) {
4560
4744
  this.mainTerm.focus();
4561
4745
  }
@@ -5067,6 +5251,79 @@ class Session {
5067
5251
  }
5068
5252
  }
5069
5253
 
5254
+ claimTerminalControl(force = false) {
5255
+ if (state.activeSessionKey !== this.key) {
5256
+ return;
5257
+ }
5258
+ if (this.socket?.readyState !== WebSocket.OPEN) {
5259
+ return;
5260
+ }
5261
+
5262
+ const now = Date.now();
5263
+ if (!force && now - this.lastTerminalControlClaimAt < 250) {
5264
+ return;
5265
+ }
5266
+
5267
+ this.lastTerminalControlClaimAt = now;
5268
+ this.send({ type: 'claim_terminal_control' });
5269
+ }
5270
+
5271
+ bindTerminalControlClaim() {
5272
+ this.unbindTerminalControlClaim();
5273
+
5274
+ const root = this.mainTerm?.element;
5275
+ if (!root) {
5276
+ return;
5277
+ }
5278
+
5279
+ const textarea = this.mainTerm.textarea
5280
+ || root.querySelector('textarea');
5281
+ const handler = () => this.claimTerminalControl();
5282
+
5283
+ root.addEventListener('mousedown', handler, true);
5284
+ root.addEventListener('touchstart', handler, true);
5285
+ if (textarea) {
5286
+ textarea.addEventListener('keydown', handler, true);
5287
+ textarea.addEventListener('paste', handler, true);
5288
+ }
5289
+
5290
+ this.boundTerminalClaimRoot = root;
5291
+ this.boundTerminalClaimTextarea = textarea;
5292
+ this.boundTerminalClaimHandler = handler;
5293
+ }
5294
+
5295
+ unbindTerminalControlClaim() {
5296
+ const handler = this.boundTerminalClaimHandler;
5297
+ if (!handler) {
5298
+ return;
5299
+ }
5300
+
5301
+ this.boundTerminalClaimRoot?.removeEventListener(
5302
+ 'mousedown',
5303
+ handler,
5304
+ true
5305
+ );
5306
+ this.boundTerminalClaimRoot?.removeEventListener(
5307
+ 'touchstart',
5308
+ handler,
5309
+ true
5310
+ );
5311
+ this.boundTerminalClaimTextarea?.removeEventListener(
5312
+ 'keydown',
5313
+ handler,
5314
+ true
5315
+ );
5316
+ this.boundTerminalClaimTextarea?.removeEventListener(
5317
+ 'paste',
5318
+ handler,
5319
+ true
5320
+ );
5321
+
5322
+ this.boundTerminalClaimRoot = null;
5323
+ this.boundTerminalClaimTextarea = null;
5324
+ this.boundTerminalClaimHandler = null;
5325
+ }
5326
+
5070
5327
  reportResize() {
5071
5328
  if (!this.isMainTerminalVisible()) {
5072
5329
  return;
@@ -5084,6 +5341,7 @@ class Session {
5084
5341
  this.shouldReconnect = false;
5085
5342
  clearTimeout(this.retryTimer);
5086
5343
  this.socket?.close();
5344
+ this.unbindTerminalControlClaim();
5087
5345
 
5088
5346
  try {
5089
5347
  if (this.previewTerm) this.previewTerm.dispose();
@@ -5124,6 +5382,9 @@ class AgentTab {
5124
5382
  this.scrollToBottomOnNextRender = true;
5125
5383
  this.busySyncTimer = null;
5126
5384
  this.planHistory = [];
5385
+ this.resumeSessions = [];
5386
+ this.resumeSessionsLoadedAt = 0;
5387
+ this.resumeSessionsPromise = null;
5127
5388
  this.update(data);
5128
5389
  this.connect();
5129
5390
  }
@@ -5143,6 +5404,7 @@ class AgentTab {
5143
5404
  }
5144
5405
 
5145
5406
  update(data) {
5407
+ const previousResumeCacheKey = `${this.agentId || ''}:${this.cwd || ''}`;
5146
5408
  this.runtimeId = data.runtimeId || '';
5147
5409
  this.runtimeKey = data.runtimeKey || '';
5148
5410
  this.acpSessionId = data.acpSessionId || '';
@@ -5163,9 +5425,17 @@ class AgentTab {
5163
5425
  this.availableCommands = Array.isArray(data.availableCommands)
5164
5426
  ? data.availableCommands
5165
5427
  : [];
5428
+ this.sessionCapabilities = normalizeAgentSessionCapabilities(
5429
+ data.sessionCapabilities || this.sessionCapabilities
5430
+ );
5166
5431
  this.configOptions = Array.isArray(data.configOptions)
5167
5432
  ? data.configOptions
5168
5433
  : [];
5434
+ const nextResumeCacheKey = `${this.agentId || ''}:${this.cwd || ''}`;
5435
+ if (previousResumeCacheKey !== nextResumeCacheKey) {
5436
+ this.resumeSessions = [];
5437
+ this.resumeSessionsLoadedAt = 0;
5438
+ }
5169
5439
  const nextPlan = Array.isArray(data.plan)
5170
5440
  ? data.plan.map((entry) => this.#normalizePlanEntry(entry))
5171
5441
  : [];
@@ -5233,6 +5503,49 @@ class AgentTab {
5233
5503
  this.#syncBusyWatchdog();
5234
5504
  }
5235
5505
 
5506
+ async listResumeSessions({ force = false } = {}) {
5507
+ if (!supportsAgentResumeCommand(this)) {
5508
+ return [];
5509
+ }
5510
+ if (
5511
+ !force
5512
+ && this.resumeSessionsLoadedAt > 0
5513
+ && (Date.now() - this.resumeSessionsLoadedAt) < 30 * 1000
5514
+ ) {
5515
+ return this.resumeSessions;
5516
+ }
5517
+ if (this.resumeSessionsPromise) {
5518
+ return this.resumeSessionsPromise;
5519
+ }
5520
+
5521
+ this.resumeSessionsPromise = (async () => {
5522
+ const cwd = this.cwd || this.getLinkedSession()?.cwd || '';
5523
+ const params = new URLSearchParams({
5524
+ agentId: this.agentId,
5525
+ cwd
5526
+ });
5527
+ const response = await this.server.fetch(
5528
+ `/api/agents/sessions?${params.toString()}`
5529
+ );
5530
+ if (!response.ok) {
5531
+ await throwResponseError(
5532
+ response,
5533
+ 'Failed to load previous sessions'
5534
+ );
5535
+ }
5536
+ const data = await response.json();
5537
+ this.resumeSessions = normalizeListedAgentSessions(data.sessions);
5538
+ this.resumeSessionsLoadedAt = Date.now();
5539
+ return this.resumeSessions;
5540
+ })();
5541
+
5542
+ try {
5543
+ return await this.resumeSessionsPromise;
5544
+ } finally {
5545
+ this.resumeSessionsPromise = null;
5546
+ }
5547
+ }
5548
+
5236
5549
  connect() {
5237
5550
  if (!this.server.isAuthenticated) return;
5238
5551
  if (
@@ -5806,6 +6119,23 @@ class AgentTab {
5806
6119
 
5807
6120
  applyInventory(data) {
5808
6121
  const previousSession = this.getLinkedSession();
6122
+ const previousSnapshot = JSON.stringify({
6123
+ runtimeId: this.runtimeId || '',
6124
+ runtimeKey: this.runtimeKey || '',
6125
+ acpSessionId: this.acpSessionId || '',
6126
+ agentId: this.agentId || '',
6127
+ agentLabel: this.agentLabel || '',
6128
+ title: this.title || '',
6129
+ commandLabel: this.commandLabel || '',
6130
+ terminalSessionId: this.terminalSessionId || '',
6131
+ cwd: this.cwd || '',
6132
+ createdAt: this.createdAt || '',
6133
+ status: this.status || 'ready',
6134
+ busy: !!this.busy,
6135
+ errorMessage: this.errorMessage || '',
6136
+ currentModeId: this.currentModeId || '',
6137
+ sessionCapabilities: this.sessionCapabilities || null
6138
+ });
5809
6139
  this.runtimeId = data.runtimeId || this.runtimeId || '';
5810
6140
  this.runtimeKey = data.runtimeKey || this.runtimeKey || '';
5811
6141
  this.acpSessionId = data.acpSessionId || this.acpSessionId || '';
@@ -5820,7 +6150,32 @@ class AgentTab {
5820
6150
  this.busy = typeof data.busy === 'boolean' ? data.busy : this.busy;
5821
6151
  this.errorMessage = data.errorMessage || this.errorMessage || '';
5822
6152
  this.currentModeId = data.currentModeId || this.currentModeId || '';
6153
+ this.sessionCapabilities = normalizeAgentSessionCapabilities(
6154
+ data.sessionCapabilities || this.sessionCapabilities
6155
+ );
5823
6156
  const nextSession = this.getLinkedSession();
6157
+ const nextSnapshot = JSON.stringify({
6158
+ runtimeId: this.runtimeId || '',
6159
+ runtimeKey: this.runtimeKey || '',
6160
+ acpSessionId: this.acpSessionId || '',
6161
+ agentId: this.agentId || '',
6162
+ agentLabel: this.agentLabel || '',
6163
+ title: this.title || '',
6164
+ commandLabel: this.commandLabel || '',
6165
+ terminalSessionId: this.terminalSessionId || '',
6166
+ cwd: this.cwd || '',
6167
+ createdAt: this.createdAt || '',
6168
+ status: this.status || 'ready',
6169
+ busy: !!this.busy,
6170
+ errorMessage: this.errorMessage || '',
6171
+ currentModeId: this.currentModeId || '',
6172
+ sessionCapabilities: this.sessionCapabilities || null
6173
+ });
6174
+ const changed = previousSnapshot !== nextSnapshot
6175
+ || previousSession?.key !== nextSession?.key;
6176
+ if (!changed) {
6177
+ return false;
6178
+ }
5824
6179
  previousSession?.updateTabUI();
5825
6180
  if (nextSession && nextSession !== previousSession) {
5826
6181
  nextSession.updateTabUI();
@@ -5828,6 +6183,7 @@ class AgentTab {
5828
6183
  if (nextSession) {
5829
6184
  refreshWorkspaceIfSessionActive(nextSession);
5830
6185
  }
6186
+ return true;
5831
6187
  }
5832
6188
 
5833
6189
  async #waitForSettled(timeoutMs = 5000) {
@@ -6374,6 +6730,27 @@ function normalizeAgentConfigOptionOptions(options) {
6374
6730
  return flattened;
6375
6731
  }
6376
6732
 
6733
+ function normalizeAgentSessionCapabilities(sessionCapabilities) {
6734
+ const source = (
6735
+ sessionCapabilities && typeof sessionCapabilities === 'object'
6736
+ )
6737
+ ? sessionCapabilities
6738
+ : {};
6739
+ return {
6740
+ load: !!source.load,
6741
+ list: !!source.list,
6742
+ resume: !!source.resume,
6743
+ fork: !!source.fork
6744
+ };
6745
+ }
6746
+
6747
+ function supportsAgentResumeCommand(agentTab) {
6748
+ const capabilities = normalizeAgentSessionCapabilities(
6749
+ agentTab?.sessionCapabilities
6750
+ );
6751
+ return !!(capabilities.load && capabilities.list);
6752
+ }
6753
+
6377
6754
  function getAgentConfigOptionById(agentTab, configId) {
6378
6755
  return normalizeAgentConfigOptions(agentTab?.configOptions).find(
6379
6756
  (option) => option.id === configId
@@ -6474,6 +6851,7 @@ function normalizeAgentCommands(commands) {
6474
6851
  : '';
6475
6852
  if (!name) return null;
6476
6853
  return {
6854
+ kind: 'command',
6477
6855
  name,
6478
6856
  description: command?.description || '',
6479
6857
  inputHint: command?.input?.hint || ''
@@ -6482,6 +6860,107 @@ function normalizeAgentCommands(commands) {
6482
6860
  .filter(Boolean);
6483
6861
  }
6484
6862
 
6863
+ function normalizeListedAgentSessions(sessions) {
6864
+ if (!Array.isArray(sessions)) return [];
6865
+ const normalized = sessions
6866
+ .map((session, index) => {
6867
+ const sessionId = String(session?.sessionId || '').trim();
6868
+ const cwd = String(session?.cwd || '').trim();
6869
+ if (!sessionId || !cwd) return null;
6870
+ return {
6871
+ kind: 'resume_session',
6872
+ sortIndex: index,
6873
+ sessionId,
6874
+ cwd,
6875
+ title: typeof session?.title === 'string'
6876
+ ? session.title
6877
+ : '',
6878
+ updatedAt: typeof session?.updatedAt === 'string'
6879
+ ? session.updatedAt
6880
+ : '',
6881
+ relativeUpdatedAt: typeof session?.relativeUpdatedAt === 'string'
6882
+ ? session.relativeUpdatedAt
6883
+ : ''
6884
+ };
6885
+ })
6886
+ .filter(Boolean);
6887
+ normalized.sort((left, right) => {
6888
+ const leftTime = Date.parse(left.updatedAt || '') || 0;
6889
+ const rightTime = Date.parse(right.updatedAt || '') || 0;
6890
+ if (leftTime !== rightTime) {
6891
+ return rightTime - leftTime;
6892
+ }
6893
+ return left.sortIndex - right.sortIndex;
6894
+ });
6895
+ return normalized;
6896
+ }
6897
+
6898
+ function getOpenAgentSessionsForServer(serverId, agentId = '') {
6899
+ const entries = Array.from(state.agentTabs.values())
6900
+ .filter((tab) => (
6901
+ tab.serverId === serverId
6902
+ && (!agentId || tab.agentId === agentId)
6903
+ ))
6904
+ .map((tab) => [String(tab.acpSessionId || '').trim(), tab])
6905
+ .filter(([sessionId]) => !!sessionId);
6906
+ return new Map(entries);
6907
+ }
6908
+
6909
+ function buildAgentResumeSessionMeta(sessionInfo) {
6910
+ const parts = [];
6911
+ const relativeUpdatedAt = String(
6912
+ sessionInfo?.relativeUpdatedAt || ''
6913
+ ).trim();
6914
+ if (relativeUpdatedAt) {
6915
+ parts.push(relativeUpdatedAt);
6916
+ }
6917
+ const timeLabel = getAgentMessageTimeLabel({
6918
+ createdAt: sessionInfo?.updatedAt || ''
6919
+ });
6920
+ if (timeLabel && !relativeUpdatedAt) {
6921
+ parts.push(timeLabel);
6922
+ }
6923
+ const cwd = String(sessionInfo?.cwd || '').trim();
6924
+ if (cwd) {
6925
+ parts.push(shortenPath(cwd, 48));
6926
+ }
6927
+ return parts.join(' · ');
6928
+ }
6929
+
6930
+ function getAgentPromptIntent(agentTab, promptValue) {
6931
+ const source = String(promptValue || '').replace(/^\s+/, '');
6932
+ const firstLine = source.split('\n', 1)[0] || '';
6933
+ if (!firstLine.startsWith('/')) {
6934
+ return { kind: 'none', query: '', commandName: '' };
6935
+ }
6936
+ const body = firstLine.slice(1);
6937
+ const [commandNameRaw = '', ...restParts] = body.split(/\s+/);
6938
+ const commandName = commandNameRaw.toLowerCase();
6939
+ const query = restParts.join(' ').trim();
6940
+ if (!commandName) {
6941
+ return { kind: 'commands', query: '', commandName: '' };
6942
+ }
6943
+ if (commandName === 'resume' && supportsAgentResumeCommand(agentTab)) {
6944
+ return {
6945
+ kind: 'resume',
6946
+ query,
6947
+ commandName
6948
+ };
6949
+ }
6950
+ if (!/\s/.test(body)) {
6951
+ return {
6952
+ kind: 'commands',
6953
+ query: commandName,
6954
+ commandName
6955
+ };
6956
+ }
6957
+ return {
6958
+ kind: 'other',
6959
+ query,
6960
+ commandName
6961
+ };
6962
+ }
6963
+
6485
6964
  function bindSingleTapActivation(element, onActivate, options = {}) {
6486
6965
  if (!element || typeof onActivate !== 'function') {
6487
6966
  return;
@@ -6643,17 +7122,19 @@ function selectAgentMessageText(previousText, nextText) {
6643
7122
  }
6644
7123
 
6645
7124
  function getAgentCommandSuggestions(agentTab, promptValue) {
6646
- if (!agentTab?.availableCommands) return [];
6647
- const source = String(promptValue || '');
6648
- const trimmed = source.replace(/^\s+/, '');
6649
- const firstLine = trimmed.split('\n', 1)[0] || '';
6650
- if (!firstLine.startsWith('/')) return [];
6651
-
6652
- const commandToken = firstLine.slice(1);
6653
- if (/\s/.test(commandToken)) return [];
6654
-
6655
- const query = commandToken.toLowerCase();
6656
- const commands = normalizeAgentCommands(agentTab.availableCommands);
7125
+ const intent = getAgentPromptIntent(agentTab, promptValue);
7126
+ if (intent.kind !== 'commands') return [];
7127
+
7128
+ const commands = normalizeAgentCommands(agentTab?.availableCommands);
7129
+ if (supportsAgentResumeCommand(agentTab)) {
7130
+ commands.unshift({
7131
+ kind: 'command',
7132
+ name: 'resume',
7133
+ description: 'Continue from a previous session',
7134
+ inputHint: ''
7135
+ });
7136
+ }
7137
+ const query = String(intent.query || '').toLowerCase();
6657
7138
  const ranked = commands.filter((command) => {
6658
7139
  const name = command.name.toLowerCase();
6659
7140
  return !query || name.startsWith(query) || name.includes(query);
@@ -6671,6 +7152,52 @@ function getAgentCommandSuggestions(agentTab, promptValue) {
6671
7152
  return ranked.slice(0, 8);
6672
7153
  }
6673
7154
 
7155
+ function getAgentResumeSuggestions(agentTab, promptValue, sessions = []) {
7156
+ const intent = getAgentPromptIntent(agentTab, promptValue);
7157
+ if (intent.kind !== 'resume') return [];
7158
+ const query = String(intent.query || '').toLowerCase();
7159
+ const openSessions = getOpenAgentSessionsForServer(
7160
+ agentTab?.serverId,
7161
+ agentTab?.agentId
7162
+ );
7163
+ const currentSessionId = String(agentTab?.acpSessionId || '').trim();
7164
+ return normalizeListedAgentSessions(sessions)
7165
+ .filter((session) => session.sessionId !== currentSessionId)
7166
+ .map((session, index) => {
7167
+ const displayName = String(
7168
+ session.title || shortenPath(session.cwd, 36)
7169
+ ).toLowerCase();
7170
+ const cwd = String(session.cwd || '').toLowerCase();
7171
+ const sessionId = String(session.sessionId || '').toLowerCase();
7172
+ const titleMatch = !query || displayName.includes(query);
7173
+ const otherMatch = !query || cwd.includes(query) || sessionId.includes(query);
7174
+ return {
7175
+ session,
7176
+ index,
7177
+ titleMatch,
7178
+ matched: titleMatch || otherMatch
7179
+ };
7180
+ })
7181
+ .filter(({ matched }) => matched)
7182
+ .sort((left, right) => {
7183
+ if (left.titleMatch !== right.titleMatch) {
7184
+ return left.titleMatch ? -1 : 1;
7185
+ }
7186
+ return left.index - right.index;
7187
+ })
7188
+ .map(({ session }) => session)
7189
+ .slice(0, 12)
7190
+ .map((session) => ({
7191
+ ...session,
7192
+ openTabKey: openSessions.get(session.sessionId)?.key || '',
7193
+ displayName: session.title || shortenPath(session.cwd, 36),
7194
+ description: [
7195
+ buildAgentResumeSessionMeta(session) || session.sessionId,
7196
+ openSessions.has(session.sessionId) ? 'Already open' : ''
7197
+ ].filter(Boolean).join(' · ')
7198
+ }));
7199
+ }
7200
+
6674
7201
  function getCurrentAgentModeLabel(agentTab) {
6675
7202
  const currentModeId = agentTab?.currentModeId || '';
6676
7203
  if (!currentModeId) return '';
@@ -9019,13 +9546,19 @@ function upsertAgentInventoryTab(server, data) {
9019
9546
  const key = makeAgentTabKey(server.id, data.id);
9020
9547
  const existing = state.agentTabs.get(key);
9021
9548
  if (existing) {
9022
- existing.applyInventory(data);
9549
+ const changed = existing.applyInventory(data);
9023
9550
  existing.connect();
9024
- return existing;
9551
+ return {
9552
+ agentTab: existing,
9553
+ changed
9554
+ };
9025
9555
  }
9026
9556
  const agentTab = new AgentTab(data, server);
9027
9557
  state.agentTabs.set(key, agentTab);
9028
- return agentTab;
9558
+ return {
9559
+ agentTab,
9560
+ changed: true
9561
+ };
9029
9562
  }
9030
9563
 
9031
9564
  function reconcileAgentInventory(server, inventory) {
@@ -9037,8 +9570,11 @@ function reconcileAgentInventory(server, inventory) {
9037
9570
  const touchedSessions = new Set();
9038
9571
 
9039
9572
  for (const tabData of Array.isArray(inventory.tabs) ? inventory.tabs : []) {
9040
- const agentTab = upsertAgentInventoryTab(server, tabData);
9573
+ const { agentTab, changed } = upsertAgentInventoryTab(server, tabData);
9041
9574
  seenKeys.add(agentTab.key);
9575
+ if (!changed) {
9576
+ continue;
9577
+ }
9042
9578
  const session = agentTab.getLinkedSession();
9043
9579
  if (session) {
9044
9580
  touchedSessions.add(session.key);
@@ -9250,16 +9786,23 @@ async function createAgentTab(session, agentId, options = {}) {
9250
9786
  await throwResponseError(response, 'Failed to create agent tab');
9251
9787
  }
9252
9788
  const data = await response.json();
9253
- const agentTab = upsertAgentTab(session.server, data);
9789
+ return await activateAgentTab(
9790
+ session,
9791
+ upsertAgentTab(session.server, data)
9792
+ );
9793
+ }
9794
+
9795
+ async function activateAgentTab(session, agentTab, options = {}) {
9796
+ if (!session || !agentTab) return null;
9797
+ const shouldSwitchSession = !!options.switchSession;
9798
+ if (shouldSwitchSession && state.activeSessionKey !== session.key) {
9799
+ await switchToSession(session.key, { scrollTabIntoView: true });
9800
+ }
9254
9801
  session.workspaceState.activeTabKey = agentTab.key;
9255
9802
  noteRecentAgentTab(session, agentTab.key);
9256
9803
  session.saveState();
9257
9804
  if (state.activeSessionKey === session.key) {
9258
- if (editorManager.currentSession?.key !== session.key) {
9259
- editorManager.switchTo(session);
9260
- }
9261
- editorManager.activateAgentTab(agentTab.key);
9262
- editorManager.updateEditorPaneVisibility();
9805
+ restoreWorkspaceForSession(session);
9263
9806
  requestAnimationFrame(() => {
9264
9807
  editorManager.agentPrompt?.focus();
9265
9808
  });
@@ -9269,6 +9812,29 @@ async function createAgentTab(session, agentId, options = {}) {
9269
9812
  return agentTab;
9270
9813
  }
9271
9814
 
9815
+ async function resumeAgentTabFromHistory(session, agentTab, historySession) {
9816
+ if (!session || !agentTab || !historySession?.sessionId) return null;
9817
+ const response = await session.server.fetch('/api/agents/tabs/resume', {
9818
+ method: 'POST',
9819
+ headers: { 'Content-Type': 'application/json' },
9820
+ body: JSON.stringify({
9821
+ agentId: agentTab.agentId,
9822
+ cwd: agentTab.cwd || session.cwd || session.initialCwd || '/',
9823
+ terminalSessionId: session.id,
9824
+ sessionId: historySession.sessionId,
9825
+ title: historySession.title || ''
9826
+ })
9827
+ });
9828
+ if (!response.ok) {
9829
+ await throwResponseError(response, 'Failed to resume agent session');
9830
+ }
9831
+ const data = await response.json();
9832
+ return await activateAgentTab(
9833
+ session,
9834
+ upsertAgentTab(session.server, data)
9835
+ );
9836
+ }
9837
+
9272
9838
  function getServerEndpointKey(server) {
9273
9839
  if (!server) return '';
9274
9840
  return getServerEndpointKeyFromUrl(server.baseUrl);
@@ -10773,6 +11339,11 @@ async function switchToSession(sessionKey, options = {}) {
10773
11339
  return;
10774
11340
  }
10775
11341
 
11342
+ const previousSession = state.activeSessionKey
11343
+ ? state.sessions.get(state.activeSessionKey)
11344
+ : null;
11345
+ previousSession?.unbindTerminalControlClaim();
11346
+
10776
11347
  state.activeSessionKey = sessionKey;
10777
11348
  renderTabs();
10778
11349
  if (scrollTabIntoView) {
@@ -10789,6 +11360,7 @@ async function switchToSession(sessionKey, options = {}) {
10789
11360
 
10790
11361
  // Mount new session
10791
11362
  session.mainTerm.open(terminalEl);
11363
+ session.bindTerminalControlClaim();
10792
11364
  session.fitMainTerminalIfVisible();
10793
11365
  if (session.isMainTerminalVisible()) {
10794
11366
  session.mainTerm.focus();
@@ -11599,6 +12171,52 @@ if (shortcutsModal) {
11599
12171
  });
11600
12172
  }
11601
12173
 
12174
+ function handleAgentCommandMenuShortcut(event) {
12175
+ const agentCommandMenuOpen = !!(
12176
+ editorManager?.agentCommandMenu
12177
+ && editorManager.agentCommandMenu.style.display !== 'none'
12178
+ && editorManager.agentCommandSuggestions.length > 0
12179
+ );
12180
+ const eventFromAgentPrompt = editorManager?.agentPrompt
12181
+ && event.target === editorManager.agentPrompt;
12182
+ if (
12183
+ !agentCommandMenuOpen
12184
+ || eventFromAgentPrompt
12185
+ || event.ctrlKey
12186
+ || event.metaKey
12187
+ || event.altKey
12188
+ ) {
12189
+ return false;
12190
+ }
12191
+ if (event.key === 'Escape') {
12192
+ event.preventDefault();
12193
+ editorManager.hideAgentCommandMenu();
12194
+ return true;
12195
+ }
12196
+ if (event.key === 'ArrowDown') {
12197
+ event.preventDefault();
12198
+ editorManager.moveAgentCommandSelection(1);
12199
+ return true;
12200
+ }
12201
+ if (event.key === 'ArrowUp') {
12202
+ event.preventDefault();
12203
+ editorManager.moveAgentCommandSelection(-1);
12204
+ return true;
12205
+ }
12206
+ if (event.key === 'Tab' || event.key === 'Enter') {
12207
+ event.preventDefault();
12208
+ void editorManager.applyAgentCommandSuggestion();
12209
+ return true;
12210
+ }
12211
+ return false;
12212
+ }
12213
+
12214
+ document.addEventListener('keydown', (e) => {
12215
+ if (handleAgentCommandMenuShortcut(e)) {
12216
+ e.stopImmediatePropagation();
12217
+ }
12218
+ }, true);
12219
+
11602
12220
  // Keyboard Shortcuts
11603
12221
  document.addEventListener('keydown', (e) => {
11604
12222
  if (