tabminal 3.0.2 → 3.0.4

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > `Tab(ter)minal`, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.
4
4
 
5
5
  `Tabminal` combines persistent server-side terminal sessions, a built-in
6
- workspace, multi-host access, and Agent Client Protocol (ACP) integrations in
6
+ workspace, multi-host access, and [Agent Client Protocol (ACP)](https://agentclientprotocol.com/get-started/introduction) integrations in
7
7
  one UI. It is designed for people who want a real terminal, real files, and
8
8
  real agent tooling without being tied to a desktop-only client let you code from
9
9
  your desktop, tablet, or phone with an intelligent, persistent, and rich
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
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": {
@@ -9,7 +9,7 @@
9
9
  "main": "src/server.mjs",
10
10
  "scripts": {
11
11
  "start": "node src/server.mjs",
12
- "dev": "node --watch src/server.mjs",
12
+ "dev": "node --watch-path=src --watch-path=public src/server.mjs",
13
13
  "build": "node build.mjs",
14
14
  "test": "node --test",
15
15
  "updep": "npx npm-check-updates -u && npm install",
package/public/app.js CHANGED
@@ -99,6 +99,7 @@ const HEARTBEAT_INTERVAL_MS = 1000;
99
99
  const RECONNECT_RETRY_MS = 5000;
100
100
  const MAIN_SERVER_ID = 'main';
101
101
  const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
102
+ const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
102
103
  const RECENT_AGENT_USAGE_STORAGE_KEY = 'tabminal_recent_agent_usage';
103
104
  const FILE_WORKSPACE_TAB_PREFIX = 'file:';
104
105
  const AGENT_WORKSPACE_TAB_PREFIX = 'agent:';
@@ -184,6 +185,96 @@ function workspaceKeyToFilePath(key) {
184
185
  return key.slice(FILE_WORKSPACE_TAB_PREFIX.length);
185
186
  }
186
187
 
188
+ function getWorkspaceDeviceId() {
189
+ try {
190
+ let value = localStorage.getItem(WORKSPACE_DEVICE_ID_STORAGE_KEY) || '';
191
+ if (!value) {
192
+ value = crypto.randomUUID();
193
+ localStorage.setItem(WORKSPACE_DEVICE_ID_STORAGE_KEY, value);
194
+ }
195
+ return value;
196
+ } catch {
197
+ return 'ephemeral-device';
198
+ }
199
+ }
200
+
201
+ function uniqueStringList(values) {
202
+ if (!Array.isArray(values)) return [];
203
+ return Array.from(new Set(
204
+ values.filter(
205
+ (value) => typeof value === 'string' && value.length > 0
206
+ )
207
+ ));
208
+ }
209
+
210
+ function normalizeWorkspaceSnapshot(input = {}, fallback = {}) {
211
+ const source = input && typeof input === 'object' ? input : {};
212
+ const base = fallback && typeof fallback === 'object' ? fallback : {};
213
+ const updatedAt = Number.isFinite(source.updatedAt)
214
+ ? source.updatedAt
215
+ : (
216
+ Number.isFinite(base.updatedAt)
217
+ ? base.updatedAt
218
+ : 0
219
+ );
220
+ const updatedBy = typeof source.updatedBy === 'string'
221
+ ? source.updatedBy
222
+ : (
223
+ typeof base.updatedBy === 'string'
224
+ ? base.updatedBy
225
+ : ''
226
+ );
227
+ return {
228
+ updatedAt,
229
+ updatedBy,
230
+ isVisible: !!source.isVisible,
231
+ openFiles: uniqueStringList(source.openFiles),
232
+ terminalDisplayMode: source.terminalDisplayMode === 'tab'
233
+ ? 'tab'
234
+ : 'auto',
235
+ expandedPaths: uniqueStringList(source.expandedPaths)
236
+ };
237
+ }
238
+
239
+ function compareWorkspaceSnapshots(left, right) {
240
+ const leftUpdatedAt = Number.isFinite(left?.updatedAt) ? left.updatedAt : 0;
241
+ const rightUpdatedAt = Number.isFinite(right?.updatedAt)
242
+ ? right.updatedAt
243
+ : 0;
244
+ if (leftUpdatedAt !== rightUpdatedAt) {
245
+ return leftUpdatedAt - rightUpdatedAt;
246
+ }
247
+ const leftUpdatedBy = typeof left?.updatedBy === 'string'
248
+ ? left.updatedBy
249
+ : '';
250
+ const rightUpdatedBy = typeof right?.updatedBy === 'string'
251
+ ? right.updatedBy
252
+ : '';
253
+ return leftUpdatedBy.localeCompare(rightUpdatedBy);
254
+ }
255
+
256
+ function buildWorkspaceSnapshotForSession(session, overrides = {}) {
257
+ return normalizeWorkspaceSnapshot({
258
+ ...session.sharedWorkspaceState,
259
+ isVisible: session.editorState.isVisible,
260
+ openFiles: session.editorState.openFiles,
261
+ terminalDisplayMode: session.sharedWorkspaceState.terminalDisplayMode,
262
+ expandedPaths: session.sharedWorkspaceState.expandedPaths,
263
+ ...overrides
264
+ });
265
+ }
266
+
267
+ function touchSharedWorkspace(session, overrides = {}) {
268
+ if (!session) return null;
269
+ const snapshot = buildWorkspaceSnapshotForSession(session, {
270
+ ...overrides,
271
+ updatedAt: Date.now(),
272
+ updatedBy: getWorkspaceDeviceId()
273
+ });
274
+ session.sharedWorkspaceState = snapshot;
275
+ return snapshot;
276
+ }
277
+
187
278
  // #region Sidebar Toggle (Mobile)
188
279
  const sidebarToggle = document.getElementById('sidebar-toggle');
189
280
  const sidebar = document.getElementById('sidebar');
@@ -616,7 +707,7 @@ class EditorManager {
616
707
  }
617
708
 
618
709
  isTerminalTabPinned(session = this.currentSession) {
619
- return session?.workspaceState?.terminalDisplayMode === 'tab';
710
+ return session?.sharedWorkspaceState?.terminalDisplayMode === 'tab';
620
711
  }
621
712
 
622
713
  canToggleTerminalWorkspaceMode(session = this.currentSession) {
@@ -694,17 +785,20 @@ class EditorManager {
694
785
  );
695
786
 
696
787
  if (isCompactWorkspaceMode()) {
697
- session.workspaceState.terminalDisplayMode = 'auto';
788
+ session.sharedWorkspaceState.terminalDisplayMode = 'auto';
698
789
  this.updateTerminalLayoutButton();
699
790
  return;
700
791
  }
701
792
 
702
- if ((session.workspaceState.terminalDisplayMode || 'auto') === nextMode) {
793
+ if (
794
+ (session.sharedWorkspaceState.terminalDisplayMode || 'auto')
795
+ === nextMode
796
+ ) {
703
797
  this.updateTerminalLayoutButton();
704
798
  return;
705
799
  }
706
800
 
707
- session.workspaceState.terminalDisplayMode = nextMode;
801
+ session.sharedWorkspaceState.terminalDisplayMode = nextMode;
708
802
  if (nextMode === 'tab') {
709
803
  session.workspaceState.activeTabKey = TERMINAL_WORKSPACE_TAB_KEY;
710
804
  } else if (
@@ -714,7 +808,7 @@ class EditorManager {
714
808
  this.getPreferredNonTerminalWorkspaceTabKey(session);
715
809
  }
716
810
 
717
- session.saveState();
811
+ session.saveState({ touchWorkspace: true });
718
812
  this.switchTo(session);
719
813
  this.updateEditorPaneVisibility();
720
814
  renderTabs();
@@ -1469,12 +1563,15 @@ class EditorManager {
1469
1563
  this.updateTerminalLayoutButton();
1470
1564
  }
1471
1565
 
1472
- toggle() {
1473
- if (!this.currentSession) return;
1474
- const state = this.currentSession.editorState;
1566
+ toggle(session = this.currentSession) {
1567
+ if (!session) return;
1568
+ const isCurrentSession = this.currentSession?.key === session.key;
1569
+ const state = session.editorState;
1475
1570
  state.isVisible = !state.isVisible;
1476
1571
 
1477
- const tab = document.querySelector(`.tab-item[data-session-key="${this.currentSession.key}"]`);
1572
+ const tab = document.querySelector(
1573
+ `.tab-item[data-session-key="${session.key}"]`
1574
+ );
1478
1575
  if (tab) {
1479
1576
  if (state.isVisible) tab.classList.add('editor-open');
1480
1577
  else tab.classList.remove('editor-open');
@@ -1482,26 +1579,34 @@ class EditorManager {
1482
1579
 
1483
1580
  if (state.isVisible) {
1484
1581
  // Only render if empty (first open)
1485
- if (this.currentSession.fileTreeElement && this.currentSession.fileTreeElement.children.length === 0) {
1486
- this.refreshSessionTree(this.currentSession);
1487
- }
1488
- this.renderEditorTabs();
1489
- const activeKey = this.getActiveWorkspaceTabKey(this.currentSession);
1490
- if (activeKey) {
1491
- this.activateWorkspaceTab(activeKey, true);
1582
+ if (
1583
+ session.fileTreeElement
1584
+ && session.fileTreeElement.children.length === 0
1585
+ ) {
1586
+ this.refreshSessionTree(session);
1492
1587
  }
1588
+ } else if (session.fileTreeElement) {
1589
+ session.fileTreeElement.innerHTML = '';
1493
1590
  }
1494
1591
 
1495
- if (this.hasCompactWorkspaceTabs(this.currentSession)) {
1592
+ if (isCurrentSession) {
1496
1593
  this.renderEditorTabs();
1497
- const activeKey = this.getActiveWorkspaceTabKey(this.currentSession);
1594
+ const activeKey = this.getActiveWorkspaceTabKey(session);
1498
1595
  if (activeKey) {
1499
1596
  this.activateWorkspaceTab(activeKey, true);
1500
1597
  }
1598
+ if (this.hasCompactWorkspaceTabs(session)) {
1599
+ this.renderEditorTabs();
1600
+ const compactActiveKey = this.getActiveWorkspaceTabKey(session);
1601
+ if (compactActiveKey) {
1602
+ this.activateWorkspaceTab(compactActiveKey, true);
1603
+ }
1604
+ }
1605
+ this.updateEditorPaneVisibility();
1501
1606
  }
1502
-
1503
- this.updateEditorPaneVisibility();
1504
- this.currentSession.saveState();
1607
+
1608
+ session.updateTabUI();
1609
+ session.saveState({ touchWorkspace: true });
1505
1610
  }
1506
1611
 
1507
1612
  switchTo(session) {
@@ -1527,6 +1632,9 @@ class EditorManager {
1527
1632
  const shouldShowWorkspace = state.isVisible
1528
1633
  || this.hasVisibleWorkspaceTabs(session);
1529
1634
  if (shouldShowWorkspace) {
1635
+ if (state.isVisible) {
1636
+ this.refreshSessionTree(session);
1637
+ }
1530
1638
  this.renderEditorTabs();
1531
1639
  const activeKey = this.getActiveWorkspaceTabKey(session);
1532
1640
  if (activeKey) {
@@ -1578,7 +1686,12 @@ class EditorManager {
1578
1686
  if (file.isDirectory) div.classList.add('is-dir');
1579
1687
 
1580
1688
  let isExpanded = false;
1581
- if (file.isDirectory && session.server.expandedPaths.has(file.path)) {
1689
+ if (
1690
+ file.isDirectory
1691
+ && session.sharedWorkspaceState.expandedPaths.includes(
1692
+ file.path
1693
+ )
1694
+ ) {
1582
1695
  isExpanded = true;
1583
1696
  li.classList.add('expanded');
1584
1697
  }
@@ -1598,11 +1711,17 @@ class EditorManager {
1598
1711
  if (file.isDirectory) {
1599
1712
  if (li.classList.contains('expanded')) {
1600
1713
  li.classList.remove('expanded');
1601
- session.server.expandedPaths.delete(file.path);
1602
- session.server.fetch('/api/memory/expand', {
1714
+ session.sharedWorkspaceState.expandedPaths =
1715
+ session.sharedWorkspaceState.expandedPaths
1716
+ .filter((path) => path !== file.path);
1717
+ session.saveState({ touchWorkspace: true });
1718
+ void session.server.fetch('/api/memory/expand', {
1603
1719
  method: 'POST',
1604
1720
  headers: { 'Content-Type': 'application/json' },
1605
- body: JSON.stringify({ path: file.path, expanded: false })
1721
+ body: JSON.stringify({
1722
+ path: file.path,
1723
+ expanded: false
1724
+ })
1606
1725
  });
1607
1726
 
1608
1727
  icon.innerHTML = this.getIcon(file.name, true, false);
@@ -1610,18 +1729,26 @@ class EditorManager {
1610
1729
  if (childUl) childUl.remove();
1611
1730
  } else {
1612
1731
  li.classList.add('expanded');
1613
- session.server.expandedPaths.add(file.path);
1614
- session.server.fetch('/api/memory/expand', {
1732
+ session.sharedWorkspaceState.expandedPaths =
1733
+ uniqueStringList([
1734
+ ...session.sharedWorkspaceState.expandedPaths,
1735
+ file.path
1736
+ ]);
1737
+ session.saveState({ touchWorkspace: true });
1738
+ void session.server.fetch('/api/memory/expand', {
1615
1739
  method: 'POST',
1616
1740
  headers: { 'Content-Type': 'application/json' },
1617
- body: JSON.stringify({ path: file.path, expanded: true })
1741
+ body: JSON.stringify({
1742
+ path: file.path,
1743
+ expanded: true
1744
+ })
1618
1745
  });
1619
1746
 
1620
1747
  icon.innerHTML = this.getIcon(file.name, true, true);
1621
1748
  await this.renderTree(file.path, li, session);
1622
1749
  }
1623
1750
  } else {
1624
- this.openFile(file.path);
1751
+ await this.openFile(file.path, session);
1625
1752
  }
1626
1753
  });
1627
1754
 
@@ -1639,13 +1766,25 @@ class EditorManager {
1639
1766
  }
1640
1767
  }
1641
1768
 
1642
- async openFile(filePath) {
1643
- if (!this.currentSession) return;
1644
- const state = this.currentSession.editorState;
1769
+ async openFile(filePath, sessionOrRestore = this.currentSession) {
1770
+ const session = typeof sessionOrRestore === 'boolean'
1771
+ ? this.currentSession
1772
+ : sessionOrRestore;
1773
+ if (!session) return;
1774
+ if (this.currentSession?.key !== session.key) {
1775
+ await switchToSession(session.key);
1776
+ }
1777
+ const targetSession = this.currentSession?.key === session.key
1778
+ ? this.currentSession
1779
+ : session;
1780
+ if (!targetSession) return;
1781
+ const state = targetSession.editorState;
1645
1782
 
1783
+ let touchedWorkspace = false;
1646
1784
  if (!state.openFiles.includes(filePath)) {
1647
1785
  state.openFiles.push(filePath);
1648
1786
  this.renderEditorTabs();
1787
+ touchedWorkspace = true;
1649
1788
  }
1650
1789
 
1651
1790
  this.updateEditorPaneVisibility();
@@ -1660,7 +1799,9 @@ class EditorManager {
1660
1799
 
1661
1800
  if (!isImage) {
1662
1801
  try {
1663
- const res = await this.currentSession.server.fetch(`/api/fs/read?path=${encodeURIComponent(filePath)}`);
1802
+ const res = await targetSession.server.fetch(
1803
+ `/api/fs/read?path=${encodeURIComponent(filePath)}`
1804
+ );
1664
1805
  if (!res.ok) throw new Error('Failed to read file');
1665
1806
  const data = await res.json();
1666
1807
  content = data.content;
@@ -1692,7 +1833,9 @@ class EditorManager {
1692
1833
  }
1693
1834
 
1694
1835
  this.activateFileTab(filePath);
1695
- this.currentSession.saveState();
1836
+ if (touchedWorkspace) {
1837
+ targetSession.saveState({ touchWorkspace: true });
1838
+ }
1696
1839
  }
1697
1840
 
1698
1841
  closeFile(filePath) {
@@ -1700,8 +1843,10 @@ class EditorManager {
1700
1843
  const state = this.currentSession.editorState;
1701
1844
 
1702
1845
  const index = state.openFiles.indexOf(filePath);
1846
+ let touchedWorkspace = false;
1703
1847
  if (index > -1) {
1704
1848
  state.openFiles.splice(index, 1);
1849
+ touchedWorkspace = true;
1705
1850
  }
1706
1851
 
1707
1852
  this.renderEditorTabs();
@@ -1727,7 +1872,9 @@ class EditorManager {
1727
1872
  }
1728
1873
 
1729
1874
  // Save state AFTER updating activeFilePath
1730
- this.currentSession.saveState();
1875
+ if (touchedWorkspace) {
1876
+ this.currentSession.saveState({ touchWorkspace: true });
1877
+ }
1731
1878
  }
1732
1879
 
1733
1880
  renderEditorTabs() {
@@ -4241,33 +4388,58 @@ class Session {
4241
4388
  this.lastExecutionEntry = null;
4242
4389
  this.needsAttention = false;
4243
4390
  this.lastNotifiedExecutionId = '';
4391
+ const legacyEditorState = data.editorState
4392
+ && typeof data.editorState === 'object'
4393
+ ? data.editorState
4394
+ : {};
4395
+ const sharedWorkspaceInput = data.workspaceState
4396
+ && typeof data.workspaceState === 'object'
4397
+ ? data.workspaceState
4398
+ : legacyEditorState;
4399
+ const hasExplicitExpandedPaths = Array.isArray(
4400
+ sharedWorkspaceInput?.expandedPaths
4401
+ );
4402
+ this.sharedWorkspaceState = normalizeWorkspaceSnapshot(
4403
+ {
4404
+ ...sharedWorkspaceInput,
4405
+ expandedPaths: hasExplicitExpandedPaths
4406
+ ? sharedWorkspaceInput.expandedPaths
4407
+ : Array.from(this.server.expandedPaths)
4408
+ }
4409
+ );
4410
+ const initialActiveFilePath = (
4411
+ typeof legacyEditorState.activeFilePath === 'string'
4412
+ && this.sharedWorkspaceState.openFiles.includes(
4413
+ legacyEditorState.activeFilePath
4414
+ )
4415
+ )
4416
+ ? legacyEditorState.activeFilePath
4417
+ : (this.sharedWorkspaceState.openFiles[0] || null);
4244
4418
 
4245
4419
  this.editorState = {
4246
- isVisible: data.editorState?.isVisible || false,
4420
+ isVisible: this.sharedWorkspaceState.isVisible,
4247
4421
  root: this.cwd,
4248
- openFiles: data.editorState?.openFiles || [],
4249
- activeFilePath: data.editorState?.activeFilePath || null,
4422
+ openFiles: [...this.sharedWorkspaceState.openFiles],
4423
+ activeFilePath: initialActiveFilePath,
4250
4424
  viewStates: new Map() // Path -> ViewState
4251
4425
  };
4252
4426
  this.workspaceState = {
4253
- activeTabKey: data.editorState?.activeWorkspaceTabKey
4254
- || (data.editorState?.activeFilePath
4255
- ? makeFileWorkspaceTabKey(data.editorState.activeFilePath)
4427
+ activeTabKey: legacyEditorState.activeWorkspaceTabKey
4428
+ || (initialActiveFilePath
4429
+ ? makeFileWorkspaceTabKey(initialActiveFilePath)
4256
4430
  : ''),
4257
- lastNonTerminalTabKey: data.editorState?.activeWorkspaceTabKey
4431
+ lastNonTerminalTabKey: legacyEditorState.activeWorkspaceTabKey
4258
4432
  && !isTerminalWorkspaceTabKey(
4259
- data.editorState.activeWorkspaceTabKey
4433
+ legacyEditorState.activeWorkspaceTabKey
4260
4434
  )
4261
- ? data.editorState.activeWorkspaceTabKey
4262
- : (data.editorState?.activeFilePath
4263
- ? makeFileWorkspaceTabKey(data.editorState.activeFilePath)
4435
+ ? legacyEditorState.activeWorkspaceTabKey
4436
+ : (initialActiveFilePath
4437
+ ? makeFileWorkspaceTabKey(initialActiveFilePath)
4264
4438
  : ''),
4265
- terminalDisplayMode:
4266
- data.editorState?.terminalDisplayMode === 'tab'
4267
- ? 'tab'
4268
- : 'auto',
4269
- recentAgentTabKeys: Array.isArray(data.editorState?.recentAgentTabKeys)
4270
- ? data.editorState.recentAgentTabKeys.filter(
4439
+ recentAgentTabKeys: Array.isArray(
4440
+ legacyEditorState?.recentAgentTabKeys
4441
+ )
4442
+ ? legacyEditorState.recentAgentTabKeys.filter(
4271
4443
  (key) => typeof key === 'string' && key.length > 0
4272
4444
  )
4273
4445
  : []
@@ -4375,8 +4547,63 @@ class Session {
4375
4547
  }
4376
4548
  }
4377
4549
 
4550
+ applySharedWorkspaceSnapshot(nextWorkspaceState) {
4551
+ const normalized = normalizeWorkspaceSnapshot(
4552
+ nextWorkspaceState,
4553
+ this.sharedWorkspaceState
4554
+ );
4555
+ const resolveFallbackActiveKey = () => {
4556
+ if (this.editorState.activeFilePath) {
4557
+ return makeFileWorkspaceTabKey(this.editorState.activeFilePath);
4558
+ }
4559
+ const agentTab = getAgentTabsForSession(this)[0];
4560
+ if (agentTab) {
4561
+ return agentTab.key;
4562
+ }
4563
+ return normalized.terminalDisplayMode === 'tab'
4564
+ ? TERMINAL_WORKSPACE_TAB_KEY
4565
+ : '';
4566
+ };
4567
+ this.sharedWorkspaceState = normalized;
4568
+ this.editorState.isVisible = normalized.isVisible;
4569
+ this.editorState.openFiles = [...normalized.openFiles];
4570
+
4571
+ if (
4572
+ this.editorState.activeFilePath
4573
+ && !this.editorState.openFiles.includes(
4574
+ this.editorState.activeFilePath
4575
+ )
4576
+ ) {
4577
+ this.editorState.activeFilePath = this.editorState.openFiles[0]
4578
+ || null;
4579
+ }
4580
+
4581
+ const activeKey = this.workspaceState.activeTabKey || '';
4582
+ if (isFileWorkspaceTabKey(activeKey)) {
4583
+ const filePath = workspaceKeyToFilePath(activeKey);
4584
+ if (!this.editorState.openFiles.includes(filePath)) {
4585
+ this.workspaceState.activeTabKey = resolveFallbackActiveKey();
4586
+ }
4587
+ } else if (
4588
+ isTerminalWorkspaceTabKey(activeKey)
4589
+ && normalized.terminalDisplayMode !== 'tab'
4590
+ ) {
4591
+ this.workspaceState.activeTabKey = resolveFallbackActiveKey();
4592
+ }
4593
+
4594
+ const lastNonTerminalKey =
4595
+ this.workspaceState.lastNonTerminalTabKey || '';
4596
+ if (isFileWorkspaceTabKey(lastNonTerminalKey)) {
4597
+ const filePath = workspaceKeyToFilePath(lastNonTerminalKey);
4598
+ if (!this.editorState.openFiles.includes(filePath)) {
4599
+ this.workspaceState.lastNonTerminalTabKey = '';
4600
+ }
4601
+ }
4602
+ }
4603
+
4378
4604
  update(data) {
4379
4605
  let changed = false;
4606
+ let workspaceChanged = false;
4380
4607
  const nextManaged = normalizeManagedSessionMeta(data.managed);
4381
4608
  if (
4382
4609
  JSON.stringify(nextManaged) !== JSON.stringify(this.managed || null)
@@ -4414,8 +4641,37 @@ class Session {
4414
4641
  this.env = data.env;
4415
4642
  changed = true;
4416
4643
  }
4417
-
4418
- if (data.cols && data.rows && (data.cols !== this.cols || data.rows !== this.rows)) {
4644
+
4645
+ const nextWorkspaceState = data.workspaceState
4646
+ && typeof data.workspaceState === 'object'
4647
+ ? data.workspaceState
4648
+ : (
4649
+ data.editorState
4650
+ && typeof data.editorState === 'object'
4651
+ ? data.editorState
4652
+ : null
4653
+ );
4654
+ if (
4655
+ nextWorkspaceState
4656
+ && compareWorkspaceSnapshots(
4657
+ nextWorkspaceState,
4658
+ this.sharedWorkspaceState
4659
+ ) > 0
4660
+ ) {
4661
+ const previousSnapshot = JSON.stringify(this.sharedWorkspaceState);
4662
+ this.applySharedWorkspaceSnapshot(nextWorkspaceState);
4663
+ const nextSnapshot = JSON.stringify(this.sharedWorkspaceState);
4664
+ if (previousSnapshot !== nextSnapshot) {
4665
+ changed = true;
4666
+ workspaceChanged = true;
4667
+ }
4668
+ }
4669
+
4670
+ if (
4671
+ data.cols
4672
+ && data.rows
4673
+ && (data.cols !== this.cols || data.rows !== this.rows)
4674
+ ) {
4419
4675
  this.cols = data.cols;
4420
4676
  this.rows = data.rows;
4421
4677
  if (this.previewTerm) {
@@ -4426,6 +4682,18 @@ class Session {
4426
4682
 
4427
4683
  if (changed) {
4428
4684
  this.updateTabUI();
4685
+ if (workspaceChanged) {
4686
+ if (this.fileTreeElement) {
4687
+ if (this.editorState.isVisible) {
4688
+ editorManager.refreshSessionTree(this);
4689
+ } else {
4690
+ this.fileTreeElement.innerHTML = '';
4691
+ }
4692
+ }
4693
+ }
4694
+ if (workspaceChanged && state.activeSessionKey === this.key) {
4695
+ refreshWorkspaceIfSessionActive(this);
4696
+ }
4429
4697
  }
4430
4698
  }
4431
4699
 
@@ -4480,6 +4748,7 @@ class Session {
4480
4748
  const tab = tabListEl.querySelector(`[data-session-key="${this.key}"]`);
4481
4749
  if (!tab) return;
4482
4750
 
4751
+ tab.classList.toggle('editor-open', !!this.editorState?.isVisible);
4483
4752
  tab.classList.toggle('agent-managed-session', isAgentManagedSession(this));
4484
4753
  tab.classList.toggle('agent-open', getAgentTabsForSession(this).length > 0);
4485
4754
 
@@ -4549,20 +4818,12 @@ class Session {
4549
4818
  }
4550
4819
  }
4551
4820
 
4552
- saveState() {
4821
+ saveState({ touchWorkspace = false } = {}) {
4822
+ if (!touchWorkspace) {
4823
+ return;
4824
+ }
4553
4825
  const pending = getPendingSession(this.key);
4554
- pending.editorState = {
4555
- isVisible: this.editorState.isVisible,
4556
- root: this.editorState.root,
4557
- openFiles: this.editorState.openFiles,
4558
- activeFilePath: this.editorState.activeFilePath,
4559
- activeWorkspaceTabKey: this.workspaceState.activeTabKey || '',
4560
- recentAgentTabKeys: Array.isArray(this.workspaceState.recentAgentTabKeys)
4561
- ? this.workspaceState.recentAgentTabKeys
4562
- : [],
4563
- terminalDisplayMode:
4564
- this.workspaceState.terminalDisplayMode || 'auto'
4565
- };
4826
+ pending.workspaceState = touchSharedWorkspace(this);
4566
4827
  }
4567
4828
 
4568
4829
  connect() {
@@ -5525,6 +5786,32 @@ class AgentTab {
5525
5786
  }
5526
5787
  }
5527
5788
 
5789
+ applyInventory(data) {
5790
+ const previousSession = this.getLinkedSession();
5791
+ this.runtimeId = data.runtimeId || this.runtimeId || '';
5792
+ this.runtimeKey = data.runtimeKey || this.runtimeKey || '';
5793
+ this.acpSessionId = data.acpSessionId || this.acpSessionId || '';
5794
+ this.agentId = data.agentId || this.agentId || '';
5795
+ this.agentLabel = data.agentLabel || this.agentLabel || 'Agent';
5796
+ this.title = typeof data.title === 'string' ? data.title : this.title;
5797
+ this.commandLabel = data.commandLabel || this.commandLabel || '';
5798
+ this.terminalSessionId = data.terminalSessionId || this.terminalSessionId;
5799
+ this.cwd = data.cwd || this.cwd || '';
5800
+ this.createdAt = data.createdAt || this.createdAt || new Date().toISOString();
5801
+ this.status = data.status || this.status || 'ready';
5802
+ this.busy = typeof data.busy === 'boolean' ? data.busy : this.busy;
5803
+ this.errorMessage = data.errorMessage || this.errorMessage || '';
5804
+ this.currentModeId = data.currentModeId || this.currentModeId || '';
5805
+ const nextSession = this.getLinkedSession();
5806
+ previousSession?.updateTabUI();
5807
+ if (nextSession && nextSession !== previousSession) {
5808
+ nextSession.updateTabUI();
5809
+ }
5810
+ if (nextSession) {
5811
+ refreshWorkspaceIfSessionActive(nextSession);
5812
+ }
5813
+ }
5814
+
5528
5815
  async #waitForSettled(timeoutMs = 5000) {
5529
5816
  const deadline = Date.now() + timeoutMs;
5530
5817
  while (Date.now() < deadline) {
@@ -5673,7 +5960,7 @@ const state = {
5673
5960
  };
5674
5961
 
5675
5962
  const pendingChanges = {
5676
- sessions: new Map() // sessionKey -> { resize, editorState, fileWrites: Map<path, content> }
5963
+ sessions: new Map() // sessionKey -> { resize, workspaceState, fileWrites: Map<path, content> }
5677
5964
  };
5678
5965
 
5679
5966
  if (typeof window !== 'undefined') {
@@ -5765,14 +6052,6 @@ function getAgentTabsForServer(serverId) {
5765
6052
  );
5766
6053
  }
5767
6054
 
5768
- function hasActiveAgentSyncNeed(serverId) {
5769
- return getAgentTabsForServer(serverId).some((tab) => (
5770
- !!tab.busy
5771
- || tab.status === 'restoring'
5772
- || tab.isDrainingQueuedPrompt
5773
- ));
5774
- }
5775
-
5776
6055
  function shouldSyncManagedTerminalSession(server, nextSummary, _previous = null) {
5777
6056
  if (!server || !nextSummary) return false;
5778
6057
  const nextSessionId = String(nextSummary.terminalSessionId || '').trim();
@@ -8491,9 +8770,32 @@ function dispatchSyntheticKey(target, init) {
8491
8770
  return event.defaultPrevented;
8492
8771
  }
8493
8772
 
8773
+ function isUiElementVisible(element) {
8774
+ if (!(element instanceof HTMLElement)) return false;
8775
+ const style = window.getComputedStyle(element);
8776
+ if (style.display === 'none' || style.visibility === 'hidden') {
8777
+ return false;
8778
+ }
8779
+ return element.getClientRects().length > 0;
8780
+ }
8781
+
8494
8782
  function getVirtualInputTarget() {
8783
+ const activeSession = getActiveSession();
8784
+ const activeWorkspaceKey = activeSession
8785
+ ? editorManager?.getActiveWorkspaceTabKey(activeSession) || ''
8786
+ : '';
8787
+ if (
8788
+ activeSession
8789
+ && isTerminalWorkspaceTabKey(activeWorkspaceKey)
8790
+ ) {
8791
+ return {
8792
+ kind: 'terminal',
8793
+ session: activeSession
8794
+ };
8795
+ }
8495
8796
  if (
8496
8797
  editorManager?.editor
8798
+ && isUiElementVisible(editorManager.monacoContainer)
8497
8799
  && typeof editorManager.editor.hasTextFocus === 'function'
8498
8800
  && editorManager.editor.hasTextFocus()
8499
8801
  ) {
@@ -8504,25 +8806,24 @@ function getVirtualInputTarget() {
8504
8806
  };
8505
8807
  }
8506
8808
  const activeElement = document.activeElement;
8507
- if (isTextEntryControl(activeElement)) {
8809
+ if (isTextEntryControl(activeElement) && isUiElementVisible(activeElement)) {
8508
8810
  return { kind: 'text', element: activeElement };
8509
8811
  }
8510
8812
  if (
8511
- state.activeSessionKey
8512
- && state.sessions.has(state.activeSessionKey)
8813
+ activeSession
8513
8814
  && terminalEl
8514
8815
  && activeElement
8515
8816
  && terminalEl.contains(activeElement)
8516
8817
  ) {
8517
8818
  return {
8518
8819
  kind: 'terminal',
8519
- session: state.sessions.get(state.activeSessionKey)
8820
+ session: activeSession
8520
8821
  };
8521
8822
  }
8522
- if (state.activeSessionKey && state.sessions.has(state.activeSessionKey)) {
8823
+ if (activeSession) {
8523
8824
  return {
8524
8825
  kind: 'terminal',
8525
- session: state.sessions.get(state.activeSessionKey)
8826
+ session: activeSession
8526
8827
  };
8527
8828
  }
8528
8829
  return { kind: 'none' };
@@ -8696,6 +8997,57 @@ function upsertAgentTab(server, data) {
8696
8997
  return agentTab;
8697
8998
  }
8698
8999
 
9000
+ function upsertAgentInventoryTab(server, data) {
9001
+ const key = makeAgentTabKey(server.id, data.id);
9002
+ const existing = state.agentTabs.get(key);
9003
+ if (existing) {
9004
+ existing.applyInventory(data);
9005
+ existing.connect();
9006
+ return existing;
9007
+ }
9008
+ const agentTab = new AgentTab(data, server);
9009
+ state.agentTabs.set(key, agentTab);
9010
+ return agentTab;
9011
+ }
9012
+
9013
+ function reconcileAgentInventory(server, inventory) {
9014
+ if (!server || !inventory || typeof inventory !== 'object') {
9015
+ return;
9016
+ }
9017
+ const restoring = !!inventory.restoring;
9018
+ const seenKeys = new Set();
9019
+ const touchedSessions = new Set();
9020
+
9021
+ for (const tabData of Array.isArray(inventory.tabs) ? inventory.tabs : []) {
9022
+ const agentTab = upsertAgentInventoryTab(server, tabData);
9023
+ seenKeys.add(agentTab.key);
9024
+ const session = agentTab.getLinkedSession();
9025
+ if (session) {
9026
+ touchedSessions.add(session.key);
9027
+ }
9028
+ }
9029
+
9030
+ if (!restoring) {
9031
+ for (const agentTab of getAgentTabsForServer(server.id)) {
9032
+ if (seenKeys.has(agentTab.key)) continue;
9033
+ const session = agentTab.getLinkedSession();
9034
+ if (session) {
9035
+ touchedSessions.add(session.key);
9036
+ }
9037
+ removeAgentTab(agentTab.key);
9038
+ }
9039
+ }
9040
+
9041
+ for (const sessionKey of touchedSessions) {
9042
+ const session = state.sessions.get(sessionKey);
9043
+ if (!session) continue;
9044
+ session.updateTabUI();
9045
+ if (state.activeSessionKey === session.key) {
9046
+ refreshWorkspaceIfSessionActive(session);
9047
+ }
9048
+ }
9049
+ }
9050
+
8699
9051
  function noteRecentAgentTab(session, agentTabKey) {
8700
9052
  if (!session || !agentTabKey) return;
8701
9053
  const recent = Array.isArray(session.workspaceState?.recentAgentTabKeys)
@@ -9098,12 +9450,17 @@ async function syncServerList() {
9098
9450
  async function fetchExpandedPaths(server) {
9099
9451
  try {
9100
9452
  const res = await server.fetch('/api/memory/expanded');
9101
- if (res.ok) {
9102
- const list = await res.json();
9103
- server.expandedPaths.clear();
9104
- list.forEach(path => server.expandedPaths.add(path));
9453
+ if (!res.ok) return;
9454
+ const list = await res.json();
9455
+ server.expandedPaths.clear();
9456
+ for (const path of Array.isArray(list) ? list : []) {
9457
+ if (typeof path === 'string' && path.length > 0) {
9458
+ server.expandedPaths.add(path);
9459
+ }
9105
9460
  }
9106
- } catch (e) { console.error(e); }
9461
+ } catch (error) {
9462
+ console.error(error);
9463
+ }
9107
9464
  }
9108
9465
 
9109
9466
  async function syncServer(server) {
@@ -9114,9 +9471,6 @@ async function syncServer(server) {
9114
9471
  const promise = (async () => {
9115
9472
  const now = Date.now();
9116
9473
  const wasReconnecting = server.connectionStatus === 'reconnecting';
9117
- const shouldRefreshAgents = !server.agentStateLoaded
9118
- || wasReconnecting
9119
- || hasActiveAgentSyncNeed(server.id);
9120
9474
  if (
9121
9475
  wasReconnecting
9122
9476
  && server.nextSyncAt
@@ -9146,8 +9500,8 @@ async function syncServer(server) {
9146
9500
  sessionUpdate.resize = pending.resize;
9147
9501
  hasUpdate = true;
9148
9502
  }
9149
- if (pending.editorState) {
9150
- sessionUpdate.editorState = pending.editorState;
9503
+ if (pending.workspaceState) {
9504
+ sessionUpdate.workspaceState = pending.workspaceState;
9151
9505
  hasUpdate = true;
9152
9506
  }
9153
9507
  if (pending.fileWrites && pending.fileWrites.size > 0) {
@@ -9197,7 +9551,7 @@ async function syncServer(server) {
9197
9551
  if (!pending) continue;
9198
9552
 
9199
9553
  if (update.resize) delete pending.resize;
9200
- if (update.editorState) delete pending.editorState;
9554
+ if (update.workspaceState) delete pending.workspaceState;
9201
9555
  if (update.fileWrites) {
9202
9556
  for (const file of update.fileWrites) {
9203
9557
  pending.fileWrites.delete(file.path);
@@ -9228,13 +9582,7 @@ async function syncServer(server) {
9228
9582
 
9229
9583
  const sessions = Array.isArray(data) ? data : data.sessions;
9230
9584
  reconcileSessions(server, sessions || []);
9231
- if (shouldRefreshAgents) {
9232
- try {
9233
- await syncAgentsForServer(server, { force: true });
9234
- } catch (error) {
9235
- console.warn('Failed to sync agents:', error);
9236
- }
9237
- }
9585
+ reconcileAgentInventory(server, data.agents);
9238
9586
  } catch (error) {
9239
9587
  if (!wasReconnecting) {
9240
9588
  console.warn(
@@ -9926,11 +10274,7 @@ function createTabElement(session) {
9926
10274
  toggleEditorBtn.title = 'Toggle File Editor';
9927
10275
  toggleEditorBtn.onclick = (e) => {
9928
10276
  e.stopPropagation();
9929
- if (state.activeSessionKey !== session.key) {
9930
- switchToSession(session.key).then(() => editorManager.toggle());
9931
- } else {
9932
- editorManager.toggle();
9933
- }
10277
+ editorManager.toggle(session);
9934
10278
  };
9935
10279
  tab.appendChild(toggleEditorBtn);
9936
10280
 
@@ -941,6 +941,106 @@ function normalizePersistedTimelineOrder(value, fallback = 0) {
941
941
  return Number.isFinite(value) && value > 0 ? value : fallback;
942
942
  }
943
943
 
944
+ function normalizeReplayMessageEntry(message = {}) {
945
+ return {
946
+ role: typeof message.role === 'string'
947
+ ? message.role
948
+ : 'assistant',
949
+ kind: typeof message.kind === 'string'
950
+ ? message.kind
951
+ : 'message',
952
+ text: typeof message.text === 'string'
953
+ ? message.text
954
+ : ''
955
+ };
956
+ }
957
+
958
+ export function createRestoreReplayState(messages = []) {
959
+ if (!Array.isArray(messages) || messages.length === 0) {
960
+ return null;
961
+ }
962
+ const replayMessages = messages
963
+ .map((message) => normalizeReplayMessageEntry(message))
964
+ .filter((message) => message.text);
965
+ if (replayMessages.length === 0) {
966
+ return null;
967
+ }
968
+ return {
969
+ messages: replayMessages,
970
+ index: -1,
971
+ offset: 0,
972
+ started: false,
973
+ exhausted: false
974
+ };
975
+ }
976
+
977
+ export function consumeRestoredMessageReplay(state, role, kind, text) {
978
+ if (!state || state.exhausted) {
979
+ return false;
980
+ }
981
+ const chunk = typeof text === 'string' ? text : '';
982
+ if (!chunk) {
983
+ return false;
984
+ }
985
+
986
+ const findReplayStart = () => {
987
+ for (let index = 0; index < state.messages.length; index += 1) {
988
+ const message = state.messages[index];
989
+ if (message.role !== role || message.kind !== kind) {
990
+ continue;
991
+ }
992
+ if (message.text.startsWith(chunk)) {
993
+ state.index = index;
994
+ state.offset = chunk.length;
995
+ state.started = true;
996
+ if (state.offset >= message.text.length) {
997
+ state.index += 1;
998
+ state.offset = 0;
999
+ }
1000
+ if (state.index >= state.messages.length) {
1001
+ state.exhausted = true;
1002
+ }
1003
+ return true;
1004
+ }
1005
+ }
1006
+ state.exhausted = true;
1007
+ return false;
1008
+ };
1009
+
1010
+ if (!state.started) {
1011
+ return findReplayStart();
1012
+ }
1013
+ if (
1014
+ state.index < 0
1015
+ || state.index >= state.messages.length
1016
+ ) {
1017
+ state.exhausted = true;
1018
+ return false;
1019
+ }
1020
+
1021
+ const message = state.messages[state.index];
1022
+ if (message.role !== role || message.kind !== kind) {
1023
+ state.exhausted = true;
1024
+ return false;
1025
+ }
1026
+
1027
+ const remaining = message.text.slice(state.offset);
1028
+ if (!remaining.startsWith(chunk)) {
1029
+ state.exhausted = true;
1030
+ return false;
1031
+ }
1032
+
1033
+ state.offset += chunk.length;
1034
+ if (state.offset >= message.text.length) {
1035
+ state.index += 1;
1036
+ state.offset = 0;
1037
+ }
1038
+ if (state.index >= state.messages.length) {
1039
+ state.exhausted = true;
1040
+ }
1041
+ return true;
1042
+ }
1043
+
944
1044
  function normalizePersistedMessage(message = {}, fallbackOrder = 0) {
945
1045
  const nextMessage = cloneSerializable(message, {}) || {};
946
1046
  nextMessage.id = typeof nextMessage.id === 'string'
@@ -1479,6 +1579,7 @@ class AcpRuntime extends EventEmitter {
1479
1579
  syntheticStreams: new Map(),
1480
1580
  syntheticStreamTurn: 0,
1481
1581
  pendingUserEcho: null,
1582
+ restoreReplay: null,
1482
1583
  currentModeId,
1483
1584
  availableModes,
1484
1585
  availableCommands,
@@ -1742,6 +1843,7 @@ class AcpRuntime extends EventEmitter {
1742
1843
  usage: meta.usage || null,
1743
1844
  terminals: meta.terminals || []
1744
1845
  });
1846
+ tab.restoreReplay = createRestoreReplayState(meta.messages || []);
1745
1847
  tab.status = 'restoring';
1746
1848
  tab.busy = true;
1747
1849
 
@@ -1777,11 +1879,13 @@ class AcpRuntime extends EventEmitter {
1777
1879
  tab.configOptions,
1778
1880
  response?.models
1779
1881
  );
1882
+ tab.restoreReplay = null;
1780
1883
  tab.status = 'ready';
1781
1884
  tab.busy = false;
1782
1885
  tab.errorMessage = '';
1783
1886
  return this.serializeTab(tab);
1784
1887
  } catch (error) {
1888
+ tab.restoreReplay = null;
1785
1889
  this.tabs.delete(tab.id);
1786
1890
  this.sessionToTabId.delete(tab.acpSessionId);
1787
1891
  throw error;
@@ -2349,6 +2453,9 @@ class AcpRuntime extends EventEmitter {
2349
2453
  if (role === 'user' && kind === 'message' && this.#consumeUserEcho(tab, text)) {
2350
2454
  return;
2351
2455
  }
2456
+ if (consumeRestoredMessageReplay(tab.restoreReplay, role, kind, text)) {
2457
+ return;
2458
+ }
2352
2459
  const streamKey = this.#getStreamKey(tab, update, role, kind);
2353
2460
  const last = tab.messages[tab.messages.length - 1] || null;
2354
2461
 
@@ -3147,6 +3254,32 @@ export class AcpManager {
3147
3254
  };
3148
3255
  }
3149
3256
 
3257
+ async listInventory() {
3258
+ return {
3259
+ restoring: this.restoring,
3260
+ tabs: Array.from(this.tabs.values()).map((entry) => {
3261
+ const serialized = entry.serialize();
3262
+ return {
3263
+ id: serialized.id,
3264
+ runtimeId: serialized.runtimeId,
3265
+ runtimeKey: serialized.runtimeKey,
3266
+ acpSessionId: serialized.acpSessionId,
3267
+ agentId: serialized.agentId,
3268
+ agentLabel: serialized.agentLabel,
3269
+ commandLabel: serialized.commandLabel,
3270
+ title: serialized.title,
3271
+ terminalSessionId: serialized.terminalSessionId,
3272
+ cwd: serialized.cwd,
3273
+ createdAt: serialized.createdAt,
3274
+ status: serialized.status,
3275
+ busy: serialized.busy,
3276
+ errorMessage: serialized.errorMessage,
3277
+ currentModeId: serialized.currentModeId
3278
+ };
3279
+ })
3280
+ };
3281
+ }
3282
+
3150
3283
  async createTab(options) {
3151
3284
  await this.ensureConfigsLoaded();
3152
3285
  const definition = this.definitions.find(
@@ -37,6 +37,7 @@ export const saveSession = async (id, data) => {
37
37
  createdAt: data.createdAt,
38
38
  // Editor State
39
39
  editorState: data.editorState || {},
40
+ workspaceState: data.editorState || {},
40
41
  executions: data.executions || []
41
42
  };
42
43
  await fs.writeFile(filePath, JSON.stringify(serializable, null, 2));
package/src/server.mjs CHANGED
@@ -216,8 +216,11 @@ router.all('/api/heartbeat', async (ctx) => {
216
216
  const { cols, rows } = update.resize;
217
217
  if (cols && rows) session.resize(cols, rows);
218
218
  }
219
- if (update.editorState) {
220
- terminalManager.updateSessionState(session.id, { editorState: update.editorState });
219
+ if (update.workspaceState || update.editorState) {
220
+ terminalManager.updateSessionState(session.id, {
221
+ workspaceState: update.workspaceState,
222
+ editorState: update.editorState
223
+ });
221
224
  }
222
225
  if (update.fileWrites) {
223
226
  for (const file of update.fileWrites) {
@@ -235,6 +238,7 @@ router.all('/api/heartbeat', async (ctx) => {
235
238
 
236
239
  ctx.body = {
237
240
  sessions: terminalManager.listSessions(),
241
+ agents: await acpManager.listInventory(),
238
242
  system: systemMonitor.getStats(),
239
243
  runtime: {
240
244
  bootId: SERVER_BOOT_ID
@@ -68,6 +68,59 @@ function clearBashPromptEnv(env) {
68
68
  }
69
69
  }
70
70
 
71
+ function uniqueStringList(values) {
72
+ if (!Array.isArray(values)) return [];
73
+ return Array.from(new Set(
74
+ values.filter(
75
+ (value) => typeof value === 'string' && value.length > 0
76
+ )
77
+ ));
78
+ }
79
+
80
+ function normalizeWorkspaceState(input = {}, fallback = {}) {
81
+ const source = input && typeof input === 'object' ? input : {};
82
+ const base = fallback && typeof fallback === 'object' ? fallback : {};
83
+ return {
84
+ updatedAt: Number.isFinite(source.updatedAt)
85
+ ? source.updatedAt
86
+ : (
87
+ Number.isFinite(base.updatedAt)
88
+ ? base.updatedAt
89
+ : 0
90
+ ),
91
+ updatedBy: typeof source.updatedBy === 'string'
92
+ ? source.updatedBy
93
+ : (
94
+ typeof base.updatedBy === 'string'
95
+ ? base.updatedBy
96
+ : ''
97
+ ),
98
+ isVisible: !!source.isVisible,
99
+ openFiles: uniqueStringList(source.openFiles),
100
+ terminalDisplayMode: source.terminalDisplayMode === 'tab'
101
+ ? 'tab'
102
+ : 'auto',
103
+ expandedPaths: uniqueStringList(source.expandedPaths)
104
+ };
105
+ }
106
+
107
+ function compareWorkspaceState(left, right) {
108
+ const leftUpdatedAt = Number.isFinite(left?.updatedAt) ? left.updatedAt : 0;
109
+ const rightUpdatedAt = Number.isFinite(right?.updatedAt)
110
+ ? right.updatedAt
111
+ : 0;
112
+ if (leftUpdatedAt !== rightUpdatedAt) {
113
+ return leftUpdatedAt - rightUpdatedAt;
114
+ }
115
+ const leftUpdatedBy = typeof left?.updatedBy === 'string'
116
+ ? left.updatedBy
117
+ : '';
118
+ const rightUpdatedBy = typeof right?.updatedBy === 'string'
119
+ ? right.updatedBy
120
+ : '';
121
+ return leftUpdatedBy.localeCompare(rightUpdatedBy);
122
+ }
123
+
71
124
  export class TerminalManager {
72
125
  constructor() {
73
126
  this.sessions = new Map();
@@ -228,7 +281,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
228
281
  removeOnExit: options.removeOnExit !== false,
229
282
  enableAiHijack: options.enableAiHijack !== false,
230
283
  enableTitlePolling: options.enableTitlePolling !== false,
231
- editorState: options.editorState,
284
+ editorState: normalizeWorkspaceState(options.editorState),
232
285
  executions: options.executions
233
286
  });
234
287
 
@@ -271,7 +324,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
271
324
  rows: restoredData?.rows,
272
325
  createdAt: restoredData?.createdAt,
273
326
  title: restoredData?.title,
274
- editorState: restoredData?.editorState,
327
+ editorState: restoredData?.workspaceState || restoredData?.editorState,
275
328
  executions: restoredData?.executions,
276
329
  restoreSnapshot: Boolean(restoredData),
277
330
  persistent: true,
@@ -326,9 +379,17 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
326
379
  updateSessionState(id, data) {
327
380
  const session = this.sessions.get(id);
328
381
  if (session) {
329
- // console.log(`[Manager] Updating session ${id} state:`, JSON.stringify(data));
330
- if (data.editorState) {
331
- session.editorState = { ...session.editorState, ...data.editorState };
382
+ const nextWorkspaceState = data.workspaceState || data.editorState;
383
+ if (nextWorkspaceState) {
384
+ const normalized = normalizeWorkspaceState(
385
+ nextWorkspaceState,
386
+ session.editorState
387
+ );
388
+ if (
389
+ compareWorkspaceState(normalized, session.editorState) > 0
390
+ ) {
391
+ session.editorState = normalized;
392
+ }
332
393
  }
333
394
  if (session.persistent) {
334
395
  this.saveSessionState(session);
@@ -418,6 +479,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
418
479
  exitStatus: s.exitStatus || null,
419
480
  managed: s.managed || null,
420
481
  editorState: s.editorState,
482
+ workspaceState: s.editorState,
421
483
  executions: s.executions
422
484
  }));
423
485
  }