tabminal 3.0.37 → 3.0.39

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.37",
3
+ "version": "3.0.39",
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
@@ -8213,6 +8213,14 @@ class EditorManager {
8213
8213
  if (!agentTab) return;
8214
8214
  const session = agentTab.getLinkedSession();
8215
8215
  if (!session) return;
8216
+ this.#renderAgentCommandSuggestions([{
8217
+ kind: 'info',
8218
+ label: 'Opening previous session...',
8219
+ description: command.displayName
8220
+ || command.title
8221
+ || command.sessionId
8222
+ || ''
8223
+ }]);
8216
8224
  try {
8217
8225
  let targetAgentTab = null;
8218
8226
  let targetPromptDraft = '';
@@ -8265,11 +8273,13 @@ class EditorManager {
8265
8273
  targetPromptDraft,
8266
8274
  targetAgentTab || getActiveAgentTab() || agentTab
8267
8275
  );
8276
+ this.hideAgentCommandMenu();
8268
8277
  } catch (error) {
8269
8278
  alert(error.message, {
8270
8279
  type: 'error',
8271
8280
  title: getAgentBaseName(agentTab)
8272
8281
  });
8282
+ this.hideAgentCommandMenu();
8273
8283
  }
8274
8284
  return;
8275
8285
  }
@@ -14719,27 +14729,54 @@ async function activateAgentTab(session, agentTab, options = {}) {
14719
14729
  return agentTab;
14720
14730
  }
14721
14731
 
14732
+ const pendingAgentHistoryResumes = new Map();
14733
+
14734
+ function getAgentHistoryResumeKey(session, agentTab, historySession) {
14735
+ return [
14736
+ session?.server?.id || '',
14737
+ session?.key || '',
14738
+ agentTab?.agentId || '',
14739
+ String(historySession?.sessionId || '').trim()
14740
+ ].join('\0');
14741
+ }
14742
+
14722
14743
  async function resumeAgentTabFromHistory(session, agentTab, historySession) {
14723
14744
  if (!session || !agentTab || !historySession?.sessionId) return null;
14724
- const response = await session.server.fetch('/api/agents/tabs/resume', {
14725
- method: 'POST',
14726
- headers: { 'Content-Type': 'application/json' },
14727
- body: JSON.stringify({
14728
- agentId: agentTab.agentId,
14729
- cwd: agentTab.cwd || session.cwd || session.initialCwd || '/',
14730
- terminalSessionId: session.id,
14731
- sessionId: historySession.sessionId,
14732
- title: historySession.title || ''
14733
- })
14734
- });
14735
- if (!response.ok) {
14736
- await throwResponseError(response, 'Failed to resume agent session');
14745
+ const resumeKey = getAgentHistoryResumeKey(session, agentTab, historySession);
14746
+ const pendingResume = pendingAgentHistoryResumes.get(resumeKey);
14747
+ if (pendingResume) {
14748
+ return await pendingResume;
14749
+ }
14750
+
14751
+ const resumePromise = (async () => {
14752
+ const response = await session.server.fetch('/api/agents/tabs/resume', {
14753
+ method: 'POST',
14754
+ headers: { 'Content-Type': 'application/json' },
14755
+ body: JSON.stringify({
14756
+ agentId: agentTab.agentId,
14757
+ cwd: agentTab.cwd || session.cwd || session.initialCwd || '/',
14758
+ terminalSessionId: session.id,
14759
+ sessionId: historySession.sessionId,
14760
+ targetTabId: agentTab.id,
14761
+ title: historySession.title || ''
14762
+ })
14763
+ });
14764
+ if (!response.ok) {
14765
+ await throwResponseError(response, 'Failed to resume agent session');
14766
+ }
14767
+ const data = await response.json();
14768
+ return await activateAgentTab(
14769
+ session,
14770
+ upsertAgentTab(session.server, data)
14771
+ );
14772
+ })();
14773
+ pendingAgentHistoryResumes.set(resumeKey, resumePromise);
14774
+
14775
+ try {
14776
+ return await resumePromise;
14777
+ } finally {
14778
+ pendingAgentHistoryResumes.delete(resumeKey);
14737
14779
  }
14738
- const data = await response.json();
14739
- return await activateAgentTab(
14740
- session,
14741
- upsertAgentTab(session.server, data)
14742
- );
14743
14780
  }
14744
14781
 
14745
14782
  function getServerEndpointKey(server) {
@@ -2733,6 +2733,108 @@ class AcpRuntime extends EventEmitter {
2733
2733
  }
2734
2734
  }
2735
2735
 
2736
+ #resetTabForSessionLoad(tab, meta) {
2737
+ tab.acpSessionId = String(meta.acpSessionId || '').trim();
2738
+ tab.terminalSessionId = meta.terminalSessionId || tab.terminalSessionId;
2739
+ tab.cwd = meta.cwd || tab.cwd;
2740
+ tab.createdAt = new Date().toISOString();
2741
+ tab.title = meta.title || '';
2742
+ tab.status = 'restoring';
2743
+ tab.busy = true;
2744
+ tab.errorMessage = '';
2745
+ tab.messages = [];
2746
+ tab.toolCalls = new Map();
2747
+ tab.permissions = new Map();
2748
+ tab.syntheticStreams = new Map();
2749
+ tab.syntheticStreamTurn = 0;
2750
+ tab.pendingUserEcho = null;
2751
+ tab.plan = [];
2752
+ tab.usage = null;
2753
+ tab.terminals = new Map();
2754
+ tab.messageCounter = 0;
2755
+ tab.timelineCounter = 0;
2756
+ tab.currentModeId = '';
2757
+ tab.availableModes = [];
2758
+ tab.availableCommands = [];
2759
+ tab.configOptions = [];
2760
+ tab.restoreCapture = createRestoreCaptureState([]);
2761
+ tab.restoreCapture.nextTimelineOrder = () =>
2762
+ this.#nextTimelineOrder(tab);
2763
+ }
2764
+
2765
+ #restoreSerializedTab(tab, snapshot) {
2766
+ tab.acpSessionId = snapshot.acpSessionId;
2767
+ tab.terminalSessionId = snapshot.terminalSessionId || '';
2768
+ tab.cwd = snapshot.cwd || tab.cwd;
2769
+ tab.createdAt = snapshot.createdAt || tab.createdAt;
2770
+ tab.status = snapshot.status || 'ready';
2771
+ tab.busy = !!snapshot.busy;
2772
+ tab.errorMessage = snapshot.errorMessage || '';
2773
+ tab.syntheticStreams = new Map();
2774
+ tab.pendingUserEcho = null;
2775
+ tab.restoreCapture = null;
2776
+ restorePersistedTabSnapshot(tab, snapshot);
2777
+ }
2778
+
2779
+ async resumeIntoTab(tabId, meta) {
2780
+ await this.start();
2781
+ this.clearIdleShutdown();
2782
+
2783
+ const sessionCapabilities = this.#getSessionCapabilities();
2784
+ if (!sessionCapabilities.load) {
2785
+ throw new Error(
2786
+ `${this.definition.label} does not support session restore`
2787
+ );
2788
+ }
2789
+
2790
+ const tab = this.tabs.get(tabId);
2791
+ if (!tab) {
2792
+ throw new Error('Agent tab not found');
2793
+ }
2794
+ if (tab.busy) {
2795
+ throw new Error('Agent tab is already running');
2796
+ }
2797
+
2798
+ const targetSessionId = String(meta.acpSessionId || '').trim();
2799
+ if (!targetSessionId) {
2800
+ throw new Error('sessionId is required');
2801
+ }
2802
+ const existingTabId = this.sessionToTabId.get(targetSessionId);
2803
+ if (existingTabId && existingTabId !== tab.id) {
2804
+ throw new Error('Session is already open');
2805
+ }
2806
+
2807
+ const previousSnapshot = this.serializeTab(tab);
2808
+ this.sessionToTabId.delete(tab.acpSessionId);
2809
+ this.#resetTabForSessionLoad(tab, {
2810
+ ...meta,
2811
+ acpSessionId: targetSessionId
2812
+ });
2813
+ this.sessionToTabId.set(tab.acpSessionId, tab.id);
2814
+ this.#broadcast(tab, {
2815
+ type: 'snapshot',
2816
+ tab: this.serializeTab(tab)
2817
+ });
2818
+
2819
+ try {
2820
+ await this.#loadSessionIntoTab(tab, meta);
2821
+ this.#broadcast(tab, {
2822
+ type: 'snapshot',
2823
+ tab: this.serializeTab(tab)
2824
+ });
2825
+ return this.serializeTab(tab);
2826
+ } catch (error) {
2827
+ this.sessionToTabId.delete(tab.acpSessionId);
2828
+ this.#restoreSerializedTab(tab, previousSnapshot);
2829
+ this.sessionToTabId.set(tab.acpSessionId, tab.id);
2830
+ this.#broadcast(tab, {
2831
+ type: 'snapshot',
2832
+ tab: this.serializeTab(tab)
2833
+ });
2834
+ throw error;
2835
+ }
2836
+ }
2837
+
2736
2838
  async #loadSessionIntoTab(tab, meta) {
2737
2839
  try {
2738
2840
  const response = await this.connection.loadSession({
@@ -3890,6 +3992,7 @@ export class AcpManager {
3890
3992
  this.loadConfigs = options.loadConfigs || persistence.loadAgentConfigs;
3891
3993
  this.saveConfigs = options.saveConfigs || persistence.saveAgentConfigs;
3892
3994
  this.persistenceChain = Promise.resolve();
3995
+ this.pendingResumeTabs = new Map();
3893
3996
  this.transcriptPersistDelayMs = options.transcriptPersistDelayMs
3894
3997
  || DEFAULT_TRANSCRIPT_PERSIST_DELAY_MS;
3895
3998
  this.persistTabsTimer = null;
@@ -4569,12 +4672,58 @@ export class AcpManager {
4569
4672
  return existingTab;
4570
4673
  }
4571
4674
 
4675
+ const resumeKey = [
4676
+ definition.id,
4677
+ String(options.sessionId || '').trim()
4678
+ ].join('\0');
4679
+ const pendingResume = this.pendingResumeTabs.get(resumeKey);
4680
+ if (pendingResume) {
4681
+ return await pendingResume;
4682
+ }
4683
+
4572
4684
  const cwd = path.resolve(options.cwd || process.cwd());
4685
+ const targetTabId = String(options.targetTabId || '').trim();
4686
+ if (targetTabId) {
4687
+ const tabEntry = this.tabs.get(targetTabId);
4688
+ if (!tabEntry) {
4689
+ throw new Error('Agent tab not found');
4690
+ }
4691
+ const currentSerialized = tabEntry.serialize();
4692
+ if (currentSerialized.agentId !== definition.id) {
4693
+ throw new Error('Agent mismatch');
4694
+ }
4695
+ const resumePromise = (async () => {
4696
+ const rawSerialized = await tabEntry.runtime.resumeIntoTab(
4697
+ targetTabId,
4698
+ {
4699
+ acpSessionId: options.sessionId,
4700
+ cwd,
4701
+ terminalSessionId: options.terminalSessionId || '',
4702
+ title: options.title || ''
4703
+ }
4704
+ );
4705
+ const serialized = this.#applyRuntimeMetadataFallback(
4706
+ tabEntry.runtime,
4707
+ rawSerialized
4708
+ );
4709
+ this.#clearDefinitionAvailabilityOverride(definition.id);
4710
+ await this.persistTabs();
4711
+ return serialized;
4712
+ })();
4713
+ this.pendingResumeTabs.set(resumeKey, resumePromise);
4714
+
4715
+ try {
4716
+ return await resumePromise;
4717
+ } finally {
4718
+ this.pendingResumeTabs.delete(resumeKey);
4719
+ }
4720
+ }
4721
+
4573
4722
  const { runtimeEntry, createdRuntime, runtimeStoreKey } =
4574
4723
  this.#ensureRuntimeEntry(definition, cwd);
4575
4724
  const tabId = crypto.randomUUID();
4576
4725
 
4577
- try {
4726
+ const resumePromise = (async () => {
4578
4727
  const rawSerialized = await runtimeEntry.runtime.resumeTab({
4579
4728
  id: tabId,
4580
4729
  acpSessionId: options.sessionId,
@@ -4603,6 +4752,11 @@ export class AcpManager {
4603
4752
  this.#clearDefinitionAvailabilityOverride(definition.id);
4604
4753
  await this.persistTabs();
4605
4754
  return tabEntry.serialize();
4755
+ })();
4756
+ this.pendingResumeTabs.set(resumeKey, resumePromise);
4757
+
4758
+ try {
4759
+ return await resumePromise;
4606
4760
  } catch (error) {
4607
4761
  const shouldDisposeRuntime = createdRuntime
4608
4762
  || runtimeEntry.runtime.tabs.size === 0;
@@ -4610,6 +4764,8 @@ export class AcpManager {
4610
4764
  await this.#disposeRuntimeEntry(runtimeStoreKey, runtimeEntry);
4611
4765
  }
4612
4766
  throw error;
4767
+ } finally {
4768
+ this.pendingResumeTabs.delete(resumeKey);
4613
4769
  }
4614
4770
  }
4615
4771
 
package/src/server.mjs CHANGED
@@ -615,7 +615,7 @@ router.post('/api/agents/tabs', async (ctx) => {
615
615
  });
616
616
 
617
617
  router.post('/api/agents/tabs/resume', async (ctx) => {
618
- const { agentId, cwd, terminalSessionId, sessionId, title } =
618
+ const { agentId, cwd, terminalSessionId, sessionId, targetTabId, title } =
619
619
  ctx.request.body || {};
620
620
  if (!agentId || typeof agentId !== 'string') {
621
621
  ctx.status = 400;
@@ -639,6 +639,7 @@ router.post('/api/agents/tabs/resume', async (ctx) => {
639
639
  agentId,
640
640
  cwd,
641
641
  sessionId,
642
+ targetTabId: typeof targetTabId === 'string' ? targetTabId : '',
642
643
  title: typeof title === 'string' ? title : '',
643
644
  terminalSessionId: typeof terminalSessionId === 'string'
644
645
  ? terminalSessionId