tabminal 3.0.9 → 3.0.11

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +268 -83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "3.0.9",
3
+ "version": "3.0.11",
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
@@ -97,6 +97,7 @@ const editorPane = document.getElementById('editor-pane');
97
97
  // #region Configuration
98
98
  const HEARTBEAT_INTERVAL_MS = 1000;
99
99
  const RECONNECT_RETRY_MS = 5000;
100
+ const FILE_TREE_REFRESH_INTERVAL_MS = 3000;
100
101
  const MAIN_SERVER_ID = 'main';
101
102
  const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
102
103
  const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
@@ -668,6 +669,7 @@ class EditorManager {
668
669
  this.currentSession = null;
669
670
  this.iconMap = null;
670
671
  this.agentTimestampTimer = null;
672
+ this.treeRefreshTimer = null;
671
673
 
672
674
  // DOM Elements
673
675
  this.pane = document.getElementById('editor-pane');
@@ -1523,8 +1525,234 @@ class EditorManager {
1523
1525
 
1524
1526
  refreshSessionTree(session) {
1525
1527
  if (!session || !session.fileTreeElement) return;
1526
- session.fileTreeElement.innerHTML = '';
1527
- this.renderTree(session.cwd, session.fileTreeElement, session);
1528
+ session.fileTreeRenderToken = (session.fileTreeRenderToken || 0) + 1;
1529
+ const renderToken = session.fileTreeRenderToken;
1530
+ const scrollTop = session.fileTreeElement.scrollTop;
1531
+ void this.renderTree(
1532
+ session.cwd,
1533
+ session.fileTreeElement,
1534
+ session,
1535
+ renderToken
1536
+ ).finally(() => {
1537
+ if (
1538
+ session.fileTreeElement
1539
+ && session.fileTreeRenderToken === renderToken
1540
+ ) {
1541
+ session.fileTreeElement.scrollTop = scrollTop;
1542
+ }
1543
+ });
1544
+ this.updateTreeAutoRefresh();
1545
+ }
1546
+
1547
+ isSessionTreeVisible(session) {
1548
+ return !!session?.fileTreeElement && !!session?.editorState?.isVisible;
1549
+ }
1550
+
1551
+ refreshVisibleSessionTrees() {
1552
+ for (const session of state.sessions.values()) {
1553
+ if (this.isSessionTreeVisible(session)) {
1554
+ this.requestSessionTreeRefresh(session);
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ requestSessionTreeRefresh(session) {
1560
+ if (!this.isSessionTreeVisible(session)) {
1561
+ this.updateTreeAutoRefresh();
1562
+ return;
1563
+ }
1564
+ if (session.fileTreeRefreshQueued) return;
1565
+ session.fileTreeRefreshQueued = true;
1566
+ requestAnimationFrame(() => {
1567
+ session.fileTreeRefreshQueued = false;
1568
+ if (this.isSessionTreeVisible(session)) {
1569
+ this.refreshSessionTree(session);
1570
+ } else {
1571
+ this.updateTreeAutoRefresh();
1572
+ }
1573
+ });
1574
+ }
1575
+
1576
+ updateTreeAutoRefresh() {
1577
+ const shouldRun = (
1578
+ document.visibilityState === 'visible'
1579
+ && Array.from(state.sessions.values()).some(
1580
+ (session) => this.isSessionTreeVisible(session)
1581
+ )
1582
+ );
1583
+ if (shouldRun && !this.treeRefreshTimer) {
1584
+ this.treeRefreshTimer = window.setInterval(() => {
1585
+ if (document.visibilityState !== 'visible') {
1586
+ this.updateTreeAutoRefresh();
1587
+ return;
1588
+ }
1589
+ const hasVisibleTrees = Array.from(state.sessions.values()).some(
1590
+ (session) => this.isSessionTreeVisible(session)
1591
+ );
1592
+ if (!hasVisibleTrees) {
1593
+ this.updateTreeAutoRefresh();
1594
+ return;
1595
+ }
1596
+ this.refreshVisibleSessionTrees();
1597
+ }, FILE_TREE_REFRESH_INTERVAL_MS);
1598
+ return;
1599
+ }
1600
+ if (!shouldRun && this.treeRefreshTimer) {
1601
+ window.clearInterval(this.treeRefreshTimer);
1602
+ this.treeRefreshTimer = null;
1603
+ }
1604
+ }
1605
+
1606
+ ensureTreeList(container) {
1607
+ const existing = Array.from(container.children).find(
1608
+ (child) => child.tagName === 'UL'
1609
+ );
1610
+ if (existing) return existing;
1611
+ const list = document.createElement('ul');
1612
+ container.appendChild(list);
1613
+ return list;
1614
+ }
1615
+
1616
+ getTreeChildList(item) {
1617
+ return Array.from(item.children).find((child) => child.tagName === 'UL')
1618
+ || null;
1619
+ }
1620
+
1621
+ getTreeItemExpanded(filePath, session) {
1622
+ return session.sharedWorkspaceState.expandedPaths.includes(filePath);
1623
+ }
1624
+
1625
+ updateTreeItem(li, file, session, renderToken) {
1626
+ li.dataset.path = file.path;
1627
+ li.dataset.isDirectory = file.isDirectory ? '1' : '0';
1628
+
1629
+ let row = Array.from(li.children).find(
1630
+ (child) => child.classList?.contains('file-tree-item')
1631
+ );
1632
+ if (!row) {
1633
+ row = document.createElement('div');
1634
+ row.className = 'file-tree-item';
1635
+ li.prepend(row);
1636
+ }
1637
+
1638
+ let icon = row.querySelector('.icon');
1639
+ if (!icon) {
1640
+ icon = document.createElement('span');
1641
+ icon.className = 'icon';
1642
+ row.appendChild(icon);
1643
+ }
1644
+
1645
+ let name = Array.from(row.children).find(
1646
+ (child) => child !== icon
1647
+ );
1648
+ if (!name) {
1649
+ name = document.createElement('span');
1650
+ row.appendChild(name);
1651
+ }
1652
+
1653
+ row.className = 'file-tree-item';
1654
+ if (file.isDirectory) {
1655
+ row.classList.add('is-dir');
1656
+ }
1657
+ row.classList.toggle(
1658
+ 'active',
1659
+ !file.isDirectory
1660
+ && session.editorState.activeFilePath === file.path
1661
+ );
1662
+
1663
+ const isExpanded = file.isDirectory
1664
+ && this.getTreeItemExpanded(file.path, session);
1665
+ li.classList.toggle('expanded', isExpanded);
1666
+ icon.innerHTML = this.getIcon(file.name, file.isDirectory, isExpanded);
1667
+ name.textContent = file.name;
1668
+
1669
+ row.onclick = async (e) => {
1670
+ e.stopPropagation();
1671
+ if (file.isDirectory) {
1672
+ if (li.classList.contains('expanded')) {
1673
+ li.classList.remove('expanded');
1674
+ session.sharedWorkspaceState.expandedPaths =
1675
+ session.sharedWorkspaceState.expandedPaths
1676
+ .filter((path) => path !== file.path);
1677
+ session.saveState({ touchWorkspace: true });
1678
+ void session.server.fetch('/api/memory/expand', {
1679
+ method: 'POST',
1680
+ headers: { 'Content-Type': 'application/json' },
1681
+ body: JSON.stringify({
1682
+ path: file.path,
1683
+ expanded: false
1684
+ })
1685
+ });
1686
+ icon.innerHTML = this.getIcon(file.name, true, false);
1687
+ const childUl = this.getTreeChildList(li);
1688
+ if (childUl) {
1689
+ childUl.remove();
1690
+ }
1691
+ this.updateTreeAutoRefresh();
1692
+ return;
1693
+ }
1694
+
1695
+ li.classList.add('expanded');
1696
+ session.sharedWorkspaceState.expandedPaths =
1697
+ uniqueStringList([
1698
+ ...session.sharedWorkspaceState.expandedPaths,
1699
+ file.path
1700
+ ]);
1701
+ session.saveState({ touchWorkspace: true });
1702
+ void session.server.fetch('/api/memory/expand', {
1703
+ method: 'POST',
1704
+ headers: { 'Content-Type': 'application/json' },
1705
+ body: JSON.stringify({
1706
+ path: file.path,
1707
+ expanded: true
1708
+ })
1709
+ });
1710
+
1711
+ icon.innerHTML = this.getIcon(file.name, true, true);
1712
+ await this.renderTree(file.path, li, session, renderToken);
1713
+ this.updateTreeAutoRefresh();
1714
+ return;
1715
+ }
1716
+
1717
+ await this.openFile(file.path, session);
1718
+ this.requestSessionTreeRefresh(session);
1719
+ };
1720
+
1721
+ if (!isExpanded) {
1722
+ const childUl = this.getTreeChildList(li);
1723
+ if (childUl) {
1724
+ childUl.remove();
1725
+ }
1726
+ }
1727
+ }
1728
+
1729
+ reconcileTreeList(list, files, session, renderToken) {
1730
+ const existingItems = new Map();
1731
+ Array.from(list.children).forEach((child) => {
1732
+ if (child.tagName === 'LI' && child.dataset.path) {
1733
+ existingItems.set(child.dataset.path, child);
1734
+ }
1735
+ });
1736
+
1737
+ const orderedItems = [];
1738
+ for (const file of files) {
1739
+ let li = existingItems.get(file.path) || null;
1740
+ if (!li) {
1741
+ li = document.createElement('li');
1742
+ } else {
1743
+ existingItems.delete(file.path);
1744
+ }
1745
+ this.updateTreeItem(li, file, session, renderToken);
1746
+ orderedItems.push(li);
1747
+ }
1748
+
1749
+ for (const li of existingItems.values()) {
1750
+ li.remove();
1751
+ }
1752
+
1753
+ for (const li of orderedItems) {
1754
+ list.appendChild(li);
1755
+ }
1528
1756
  }
1529
1757
 
1530
1758
  initMonaco() {
@@ -1656,6 +1884,7 @@ class EditorManager {
1656
1884
  this.updateEditorPaneVisibility();
1657
1885
  }
1658
1886
 
1887
+ this.updateTreeAutoRefresh();
1659
1888
  session.updateTabUI();
1660
1889
  session.saveState({ touchWorkspace: true });
1661
1890
  }
@@ -1694,6 +1923,7 @@ class EditorManager {
1694
1923
 
1695
1924
  this.updateEditorPaneVisibility();
1696
1925
  this.updateTerminalLayoutButton();
1926
+ this.updateTreeAutoRefresh();
1697
1927
 
1698
1928
  // Restore layout
1699
1929
  if (session.layoutState) {
@@ -1719,96 +1949,37 @@ class EditorManager {
1719
1949
  }
1720
1950
  }
1721
1951
 
1722
- async renderTree(dirPath, container, session) {
1952
+ async renderTree(
1953
+ dirPath,
1954
+ container,
1955
+ session,
1956
+ renderToken = session?.fileTreeRenderToken || 0
1957
+ ) {
1723
1958
  try {
1724
- const res = await session.server.fetch(`/api/fs/list?path=${encodeURIComponent(dirPath)}`);
1959
+ const res = await session.server.fetch(
1960
+ `/api/fs/list?path=${encodeURIComponent(dirPath)}`
1961
+ );
1725
1962
  if (!res.ok) return;
1726
1963
  const files = await res.json();
1964
+ if ((session.fileTreeRenderToken || 0) !== renderToken) return;
1965
+
1966
+ const list = this.ensureTreeList(container);
1967
+ this.reconcileTreeList(list, files, session, renderToken);
1968
+ if ((session.fileTreeRenderToken || 0) !== renderToken) return;
1727
1969
 
1728
- const ul = document.createElement('ul');
1729
-
1730
1970
  for (const file of files) {
1731
- const li = document.createElement('li');
1732
- const div = document.createElement('div');
1733
- div.className = 'file-tree-item';
1734
- if (file.isDirectory) div.classList.add('is-dir');
1735
-
1736
- let isExpanded = false;
1737
1971
  if (
1738
1972
  file.isDirectory
1739
- && session.sharedWorkspaceState.expandedPaths.includes(
1740
- file.path
1741
- )
1973
+ && this.getTreeItemExpanded(file.path, session)
1742
1974
  ) {
1743
- isExpanded = true;
1744
- li.classList.add('expanded');
1745
- }
1746
-
1747
- const icon = document.createElement('span');
1748
- icon.className = 'icon';
1749
- icon.innerHTML = this.getIcon(file.name, file.isDirectory, isExpanded);
1750
-
1751
- const name = document.createElement('span');
1752
- name.textContent = file.name;
1753
-
1754
- div.appendChild(icon);
1755
- div.appendChild(name);
1756
-
1757
- div.addEventListener('click', async (e) => {
1758
- e.stopPropagation();
1759
- if (file.isDirectory) {
1760
- if (li.classList.contains('expanded')) {
1761
- li.classList.remove('expanded');
1762
- session.sharedWorkspaceState.expandedPaths =
1763
- session.sharedWorkspaceState.expandedPaths
1764
- .filter((path) => path !== file.path);
1765
- session.saveState({ touchWorkspace: true });
1766
- void session.server.fetch('/api/memory/expand', {
1767
- method: 'POST',
1768
- headers: { 'Content-Type': 'application/json' },
1769
- body: JSON.stringify({
1770
- path: file.path,
1771
- expanded: false
1772
- })
1773
- });
1774
-
1775
- icon.innerHTML = this.getIcon(file.name, true, false);
1776
- const childUl = li.querySelector('ul');
1777
- if (childUl) childUl.remove();
1778
- } else {
1779
- li.classList.add('expanded');
1780
- session.sharedWorkspaceState.expandedPaths =
1781
- uniqueStringList([
1782
- ...session.sharedWorkspaceState.expandedPaths,
1783
- file.path
1784
- ]);
1785
- session.saveState({ touchWorkspace: true });
1786
- void session.server.fetch('/api/memory/expand', {
1787
- method: 'POST',
1788
- headers: { 'Content-Type': 'application/json' },
1789
- body: JSON.stringify({
1790
- path: file.path,
1791
- expanded: true
1792
- })
1793
- });
1794
-
1795
- icon.innerHTML = this.getIcon(file.name, true, true);
1796
- await this.renderTree(file.path, li, session);
1797
- }
1798
- } else {
1799
- await this.openFile(file.path, session);
1975
+ const item = Array.from(list.children).find(
1976
+ (child) => child.dataset.path === file.path
1977
+ );
1978
+ if (item) {
1979
+ void this.renderTree(file.path, item, session, renderToken);
1800
1980
  }
1801
- });
1802
-
1803
- li.appendChild(div);
1804
-
1805
- if (isExpanded) {
1806
- this.renderTree(file.path, li, session);
1807
1981
  }
1808
-
1809
- ul.appendChild(li);
1810
1982
  }
1811
- container.appendChild(ul);
1812
1983
  } catch (err) {
1813
1984
  console.error('Failed to render tree:', err);
1814
1985
  }
@@ -4884,11 +5055,12 @@ class Session {
4884
5055
  if (workspaceChanged) {
4885
5056
  if (this.fileTreeElement) {
4886
5057
  if (this.editorState.isVisible) {
4887
- editorManager.refreshSessionTree(this);
5058
+ editorManager.requestSessionTreeRefresh(this);
4888
5059
  } else {
4889
5060
  this.fileTreeElement.innerHTML = '';
4890
5061
  }
4891
5062
  }
5063
+ editorManager.updateTreeAutoRefresh();
4892
5064
  }
4893
5065
  if (workspaceChanged && state.activeSessionKey === this.key) {
4894
5066
  refreshWorkspaceIfSessionActive(this);
@@ -5146,6 +5318,9 @@ class Session {
5146
5318
  this.runningCommand = '';
5147
5319
  this.needsAttention = false;
5148
5320
  this.updateTabUI();
5321
+ if (this.editorState.isVisible) {
5322
+ editorManager.requestSessionTreeRefresh(this);
5323
+ }
5149
5324
  if (state.activeSessionKey === this.key) {
5150
5325
  editorManager.renderEditorTabs();
5151
5326
  }
@@ -5205,6 +5380,10 @@ class Session {
5205
5380
  this.needsAttention = false;
5206
5381
  }
5207
5382
 
5383
+ if (this.editorState.isVisible) {
5384
+ editorManager.requestSessionTreeRefresh(this);
5385
+ }
5386
+
5208
5387
  this.updateTabUI();
5209
5388
  if (state.activeSessionKey === this.key) {
5210
5389
  editorManager.renderEditorTabs();
@@ -10876,7 +11055,7 @@ function createTabElement(session) {
10876
11055
  session.fileTreeElement = fileTree;
10877
11056
 
10878
11057
  if (session.editorState && session.editorState.isVisible) {
10879
- editorManager.renderTree(session.cwd, fileTree, session);
11058
+ editorManager.refreshSessionTree(session);
10880
11059
  }
10881
11060
  tab.appendChild(fileTree);
10882
11061
 
@@ -11244,10 +11423,14 @@ document.addEventListener('keydown', noteAppInteraction, {
11244
11423
  window.addEventListener('focus', () => {
11245
11424
  noteAppInteraction();
11246
11425
  enterAppNotificationQuietPeriod();
11426
+ editorManager.refreshVisibleSessionTrees();
11427
+ editorManager.updateTreeAutoRefresh();
11247
11428
  });
11248
11429
  window.addEventListener('pageshow', () => {
11249
11430
  noteAppInteraction();
11250
11431
  enterAppNotificationQuietPeriod();
11432
+ editorManager.refreshVisibleSessionTrees();
11433
+ editorManager.updateTreeAutoRefresh();
11251
11434
  });
11252
11435
 
11253
11436
  document.addEventListener('click', () => {
@@ -11258,7 +11441,9 @@ document.addEventListener('visibilitychange', () => {
11258
11441
  noteAppInteraction();
11259
11442
  enterAppNotificationQuietPeriod();
11260
11443
  clearVisibleAttentionState();
11444
+ editorManager.refreshVisibleSessionTrees();
11261
11445
  }
11446
+ editorManager.updateTreeAutoRefresh();
11262
11447
  });
11263
11448
  // #endregion
11264
11449