tabminal 3.0.11 → 3.0.13

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
@@ -90,6 +90,12 @@ const agentSetupCopilotToken = document.getElementById(
90
90
  const agentSetupCopilotNote = document.getElementById(
91
91
  'agent-setup-copilot-note'
92
92
  );
93
+ const confirmModal = document.getElementById('confirm-modal');
94
+ const confirmModalTitle = document.getElementById('confirm-modal-title');
95
+ const confirmModalMessage = document.getElementById('confirm-modal-message');
96
+ const confirmModalNote = document.getElementById('confirm-modal-note');
97
+ const confirmModalCancel = document.getElementById('confirm-modal-cancel');
98
+ const confirmModalConfirm = document.getElementById('confirm-modal-confirm');
93
99
  const terminalWrapper = document.getElementById('terminal-wrapper');
94
100
  const editorPane = document.getElementById('editor-pane');
95
101
  // #endregion
@@ -105,6 +111,14 @@ const RECENT_AGENT_USAGE_STORAGE_KEY = 'tabminal_recent_agent_usage';
105
111
  const FILE_WORKSPACE_TAB_PREFIX = 'file:';
106
112
  const AGENT_WORKSPACE_TAB_PREFIX = 'agent:';
107
113
  const TERMINAL_WORKSPACE_TAB_KEY = 'terminal:main';
114
+ const SUPPORTED_IMAGE_EXTENSIONS = new Set([
115
+ 'png',
116
+ 'jpg',
117
+ 'jpeg',
118
+ 'gif',
119
+ 'svg',
120
+ 'webp'
121
+ ]);
108
122
  const CLOSE_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
109
123
  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>';
110
124
  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>';
@@ -119,6 +133,10 @@ const THOUGHT_SELECT_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15"
119
133
  const TERMINAL_TAB_MODE_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="4" y="5" width="16" height="14" rx="2"></rect><path d="M4 9h16"></path><path d="m9 15 3-3 3 3"></path></svg>';
120
134
  const TERMINAL_AUTO_MODE_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="4" y="5" width="16" height="5" rx="1.5"></rect><rect x="4" y="14" width="16" height="5" rx="1.5"></rect></svg>';
121
135
  const PLUS_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="M12 5v14"></path><path d="M5 12h14"></path></svg>';
136
+ const RENAME_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="m12 20 7-7"></path><path d="M16 6.5a1.8 1.8 0 1 1 2.5 2.5L8 19.5 4 20l.5-4L16 6.5Z"></path></svg>';
137
+ const DELETE_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M6 7l1 12h10l1-12"></path><path d="M9 7V4h6v3"></path></svg>';
138
+ const NEW_FOLDER_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7.5A2.5 2.5 0 0 1 6 5h4l2 2h6a2.5 2.5 0 0 1 2.5 2.5V17A2.5 2.5 0 0 1 18 19.5H6A2.5 2.5 0 0 1 3.5 17Z"></path><path d="M12 10.5v5"></path><path d="M9.5 13h5"></path></svg>';
139
+ const NEW_FILE_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3.5h7l4 4V20.5H7A2.5 2.5 0 0 1 4.5 18V6A2.5 2.5 0 0 1 7 3.5Z"></path><path d="M14 3.5V8h4"></path><path d="M12 11v6"></path><path d="M9 14h6"></path></svg>';
122
140
  const TERMINAL_FONT_FAMILY = '\'Monaspace Neon\', "SF Mono Terminal", '
123
141
  + '"SFMono-Regular", "SF Mono", "JetBrains Mono", Menlo, Consolas, '
124
142
  + 'monospace';
@@ -171,6 +189,18 @@ function isCompactWorkspaceMode() {
171
189
  return !!window.__tabminalCompactWorkspaceMode;
172
190
  }
173
191
 
192
+ function isSupportedImagePath(filePath) {
193
+ if (typeof filePath !== 'string') {
194
+ return false;
195
+ }
196
+ const dotIndex = filePath.lastIndexOf('.');
197
+ if (dotIndex === -1) {
198
+ return false;
199
+ }
200
+ const ext = filePath.slice(dotIndex + 1).toLowerCase();
201
+ return SUPPORTED_IMAGE_EXTENSIONS.has(ext);
202
+ }
203
+
174
204
  function isCompactTerminalTabsMode() {
175
205
  return !!window.__tabminalCompactTerminalTabsMode;
176
206
  }
@@ -1455,6 +1485,590 @@ class EditorManager {
1455
1485
  store.set(filePath, value);
1456
1486
  }
1457
1487
 
1488
+ remapTreePath(pathValue, oldPath, newPath, isDirectory) {
1489
+ if (typeof pathValue !== 'string' || pathValue.length === 0) {
1490
+ return pathValue;
1491
+ }
1492
+ if (pathValue === oldPath) {
1493
+ return newPath;
1494
+ }
1495
+ if (
1496
+ isDirectory
1497
+ && pathValue.startsWith(`${oldPath}/`)
1498
+ ) {
1499
+ return `${newPath}${pathValue.slice(oldPath.length)}`;
1500
+ }
1501
+ return pathValue;
1502
+ }
1503
+
1504
+ remapWorkspaceTabKey(key, oldPath, newPath, isDirectory) {
1505
+ if (!isFileWorkspaceTabKey(key)) return key;
1506
+ const filePath = workspaceKeyToFilePath(key);
1507
+ const nextPath = this.remapTreePath(
1508
+ filePath,
1509
+ oldPath,
1510
+ newPath,
1511
+ isDirectory
1512
+ );
1513
+ return nextPath ? makeFileWorkspaceTabKey(nextPath) : key;
1514
+ }
1515
+
1516
+ cloneRenamedModelEntry(entry, nextPath) {
1517
+ if (!entry || typeof entry !== 'object') return entry;
1518
+ const nextEntry = {
1519
+ ...entry
1520
+ };
1521
+ if (nextEntry.model) {
1522
+ let nextContent = nextEntry.content;
1523
+ try {
1524
+ if (typeof nextEntry.model.getValue === 'function') {
1525
+ nextContent = nextEntry.model.getValue();
1526
+ }
1527
+ } catch {
1528
+ // Ignore content extraction failure and keep cached content.
1529
+ }
1530
+ nextEntry.content = nextContent;
1531
+
1532
+ if (
1533
+ this.monacoInstance
1534
+ && typeof nextEntry.model.getLanguageId === 'function'
1535
+ ) {
1536
+ const oldModel = nextEntry.model;
1537
+ const languageId = oldModel.getLanguageId();
1538
+ const uri = this.monacoInstance.Uri.file(nextPath);
1539
+ const existingModel = this.monacoInstance.editor.getModel(uri);
1540
+ if (existingModel && existingModel !== oldModel) {
1541
+ existingModel.setValue(nextContent ?? '');
1542
+ nextEntry.model = existingModel;
1543
+ } else {
1544
+ nextEntry.model = this.monacoInstance.editor.createModel(
1545
+ nextContent ?? '',
1546
+ languageId,
1547
+ uri
1548
+ );
1549
+ }
1550
+ if (nextEntry.model !== oldModel) {
1551
+ try {
1552
+ oldModel.dispose();
1553
+ } catch {
1554
+ // Ignore disposal failures for stale models.
1555
+ }
1556
+ }
1557
+ return nextEntry;
1558
+ }
1559
+ }
1560
+ return nextEntry;
1561
+ }
1562
+
1563
+ remapModelStorePaths(server, oldPath, newPath, isDirectory) {
1564
+ if (!server?.modelStore) return false;
1565
+ const nextEntries = [];
1566
+ let changed = false;
1567
+ for (const [path, entry] of server.modelStore.entries()) {
1568
+ const nextPath = this.remapTreePath(
1569
+ path,
1570
+ oldPath,
1571
+ newPath,
1572
+ isDirectory
1573
+ );
1574
+ if (nextPath !== path) {
1575
+ changed = true;
1576
+ nextEntries.push([
1577
+ nextPath,
1578
+ this.cloneRenamedModelEntry(entry, nextPath)
1579
+ ]);
1580
+ server.modelStore.delete(path);
1581
+ }
1582
+ }
1583
+ for (const [nextPath, entry] of nextEntries) {
1584
+ server.modelStore.set(nextPath, entry);
1585
+ }
1586
+ return changed;
1587
+ }
1588
+
1589
+ remapPendingFileWrites(sessionKey, oldPath, newPath, isDirectory) {
1590
+ const pending = pendingChanges.sessions.get(sessionKey);
1591
+ if (!pending?.fileWrites || pending.fileWrites.size === 0) {
1592
+ return false;
1593
+ }
1594
+ const nextEntries = [];
1595
+ let changed = false;
1596
+ for (const [path, content] of pending.fileWrites.entries()) {
1597
+ const nextPath = this.remapTreePath(
1598
+ path,
1599
+ oldPath,
1600
+ newPath,
1601
+ isDirectory
1602
+ );
1603
+ if (nextPath !== path) {
1604
+ changed = true;
1605
+ pending.fileWrites.delete(path);
1606
+ nextEntries.push([nextPath, content]);
1607
+ }
1608
+ }
1609
+ for (const [nextPath, content] of nextEntries) {
1610
+ pending.fileWrites.set(nextPath, content);
1611
+ }
1612
+ return changed;
1613
+ }
1614
+
1615
+ pathMatchesTarget(pathValue, targetPath, isDirectory) {
1616
+ if (typeof pathValue !== 'string' || pathValue.length === 0) {
1617
+ return false;
1618
+ }
1619
+ if (pathValue === targetPath) {
1620
+ return true;
1621
+ }
1622
+ return !!(
1623
+ isDirectory
1624
+ && pathValue.startsWith(`${targetPath}/`)
1625
+ );
1626
+ }
1627
+
1628
+ removeDeletedModelStorePaths(server, targetPath, isDirectory) {
1629
+ if (!server?.modelStore) return false;
1630
+ let changed = false;
1631
+ for (const [path, entry] of [...server.modelStore.entries()]) {
1632
+ if (!this.pathMatchesTarget(path, targetPath, isDirectory)) {
1633
+ continue;
1634
+ }
1635
+ changed = true;
1636
+ try {
1637
+ entry?.model?.dispose?.();
1638
+ } catch {
1639
+ // Ignore stale model disposal failures.
1640
+ }
1641
+ server.modelStore.delete(path);
1642
+ }
1643
+ return changed;
1644
+ }
1645
+
1646
+ removeDeletedPendingFileWrites(sessionKey, targetPath, isDirectory) {
1647
+ const pending = pendingChanges.sessions.get(sessionKey);
1648
+ if (!pending?.fileWrites || pending.fileWrites.size === 0) {
1649
+ return false;
1650
+ }
1651
+ let changed = false;
1652
+ for (const path of [...pending.fileWrites.keys()]) {
1653
+ if (!this.pathMatchesTarget(path, targetPath, isDirectory)) {
1654
+ continue;
1655
+ }
1656
+ changed = true;
1657
+ pending.fileWrites.delete(path);
1658
+ }
1659
+ return changed;
1660
+ }
1661
+
1662
+ applyRenamedPathToSession(session, oldPath, newPath, isDirectory) {
1663
+ let workspaceChanged = false;
1664
+ let visualChanged = false;
1665
+
1666
+ const remapList = (values) => {
1667
+ const nextValues = [];
1668
+ for (const value of values) {
1669
+ const nextValue = this.remapTreePath(
1670
+ value,
1671
+ oldPath,
1672
+ newPath,
1673
+ isDirectory
1674
+ );
1675
+ if (!nextValues.includes(nextValue)) {
1676
+ nextValues.push(nextValue);
1677
+ }
1678
+ }
1679
+ return nextValues;
1680
+ };
1681
+
1682
+ const nextOpenFiles = remapList(session.editorState.openFiles);
1683
+ if (
1684
+ JSON.stringify(nextOpenFiles)
1685
+ !== JSON.stringify(session.editorState.openFiles)
1686
+ ) {
1687
+ session.editorState.openFiles = nextOpenFiles;
1688
+ session.sharedWorkspaceState.openFiles = [...nextOpenFiles];
1689
+ workspaceChanged = true;
1690
+ visualChanged = true;
1691
+ }
1692
+
1693
+ const nextExpandedPaths = remapList(
1694
+ session.sharedWorkspaceState.expandedPaths
1695
+ );
1696
+ if (
1697
+ JSON.stringify(nextExpandedPaths)
1698
+ !== JSON.stringify(session.sharedWorkspaceState.expandedPaths)
1699
+ ) {
1700
+ session.sharedWorkspaceState.expandedPaths = nextExpandedPaths;
1701
+ workspaceChanged = true;
1702
+ }
1703
+
1704
+ const nextActiveFilePath = this.remapTreePath(
1705
+ session.editorState.activeFilePath,
1706
+ oldPath,
1707
+ newPath,
1708
+ isDirectory
1709
+ );
1710
+ if (nextActiveFilePath !== session.editorState.activeFilePath) {
1711
+ session.editorState.activeFilePath = nextActiveFilePath || null;
1712
+ visualChanged = true;
1713
+ }
1714
+
1715
+ const nextActiveTabKey = this.remapWorkspaceTabKey(
1716
+ session.workspaceState.activeTabKey,
1717
+ oldPath,
1718
+ newPath,
1719
+ isDirectory
1720
+ );
1721
+ if (nextActiveTabKey !== session.workspaceState.activeTabKey) {
1722
+ session.workspaceState.activeTabKey = nextActiveTabKey;
1723
+ visualChanged = true;
1724
+ }
1725
+
1726
+ const nextLastNonTerminalTabKey = this.remapWorkspaceTabKey(
1727
+ session.workspaceState.lastNonTerminalTabKey,
1728
+ oldPath,
1729
+ newPath,
1730
+ isDirectory
1731
+ );
1732
+ if (
1733
+ nextLastNonTerminalTabKey
1734
+ !== session.workspaceState.lastNonTerminalTabKey
1735
+ ) {
1736
+ session.workspaceState.lastNonTerminalTabKey =
1737
+ nextLastNonTerminalTabKey;
1738
+ }
1739
+
1740
+ if (session.editorState.viewStates.size > 0) {
1741
+ const nextViewStates = new Map();
1742
+ for (const [path, viewState] of session.editorState.viewStates) {
1743
+ nextViewStates.set(
1744
+ this.remapTreePath(path, oldPath, newPath, isDirectory),
1745
+ viewState
1746
+ );
1747
+ }
1748
+ session.editorState.viewStates = nextViewStates;
1749
+ }
1750
+
1751
+ const nextSelectedTreePath = this.remapTreePath(
1752
+ session.selectedTreePath,
1753
+ oldPath,
1754
+ newPath,
1755
+ isDirectory
1756
+ );
1757
+ if (nextSelectedTreePath !== session.selectedTreePath) {
1758
+ session.selectedTreePath = nextSelectedTreePath || '';
1759
+ visualChanged = true;
1760
+ }
1761
+
1762
+ const nextEditingTreePath = this.remapTreePath(
1763
+ session.treeEditingPath,
1764
+ oldPath,
1765
+ newPath,
1766
+ isDirectory
1767
+ );
1768
+ if (nextEditingTreePath !== session.treeEditingPath) {
1769
+ session.treeEditingPath = nextEditingTreePath || '';
1770
+ }
1771
+
1772
+ const nextPendingFocusPath = this.remapTreePath(
1773
+ session.pendingTreeFocusPath,
1774
+ oldPath,
1775
+ newPath,
1776
+ isDirectory
1777
+ );
1778
+ if (nextPendingFocusPath !== session.pendingTreeFocusPath) {
1779
+ session.pendingTreeFocusPath = nextPendingFocusPath || '';
1780
+ }
1781
+
1782
+ const nextPendingRenameFocusPath = this.remapTreePath(
1783
+ session.pendingTreeRenameFocusPath,
1784
+ oldPath,
1785
+ newPath,
1786
+ isDirectory
1787
+ );
1788
+ if (
1789
+ nextPendingRenameFocusPath
1790
+ !== session.pendingTreeRenameFocusPath
1791
+ ) {
1792
+ session.pendingTreeRenameFocusPath =
1793
+ nextPendingRenameFocusPath || '';
1794
+ }
1795
+
1796
+ return {
1797
+ workspaceChanged,
1798
+ visualChanged
1799
+ };
1800
+ }
1801
+
1802
+ applyDeletedPathToSession(session, targetPath, isDirectory) {
1803
+ let workspaceChanged = false;
1804
+ let visualChanged = false;
1805
+
1806
+ const filterList = (values) => values.filter(
1807
+ (value) => !this.pathMatchesTarget(value, targetPath, isDirectory)
1808
+ );
1809
+
1810
+ const nextOpenFiles = filterList(session.editorState.openFiles);
1811
+ if (
1812
+ JSON.stringify(nextOpenFiles)
1813
+ !== JSON.stringify(session.editorState.openFiles)
1814
+ ) {
1815
+ session.editorState.openFiles = nextOpenFiles;
1816
+ session.sharedWorkspaceState.openFiles = [...nextOpenFiles];
1817
+ workspaceChanged = true;
1818
+ visualChanged = true;
1819
+ }
1820
+
1821
+ const nextExpandedPaths = filterList(
1822
+ session.sharedWorkspaceState.expandedPaths
1823
+ );
1824
+ if (
1825
+ JSON.stringify(nextExpandedPaths)
1826
+ !== JSON.stringify(session.sharedWorkspaceState.expandedPaths)
1827
+ ) {
1828
+ session.sharedWorkspaceState.expandedPaths = nextExpandedPaths;
1829
+ workspaceChanged = true;
1830
+ }
1831
+
1832
+ if (
1833
+ this.pathMatchesTarget(
1834
+ session.editorState.activeFilePath,
1835
+ targetPath,
1836
+ isDirectory
1837
+ )
1838
+ ) {
1839
+ session.editorState.activeFilePath = nextOpenFiles[0] || null;
1840
+ visualChanged = true;
1841
+ }
1842
+
1843
+ if (session.editorState.viewStates.size > 0) {
1844
+ const nextViewStates = new Map();
1845
+ let changed = false;
1846
+ for (const [path, viewState] of session.editorState.viewStates) {
1847
+ if (this.pathMatchesTarget(path, targetPath, isDirectory)) {
1848
+ changed = true;
1849
+ continue;
1850
+ }
1851
+ nextViewStates.set(path, viewState);
1852
+ }
1853
+ if (changed) {
1854
+ session.editorState.viewStates = nextViewStates;
1855
+ }
1856
+ }
1857
+
1858
+ if (
1859
+ this.pathMatchesTarget(
1860
+ session.selectedTreePath,
1861
+ targetPath,
1862
+ isDirectory
1863
+ )
1864
+ ) {
1865
+ session.selectedTreePath = '';
1866
+ visualChanged = true;
1867
+ }
1868
+
1869
+ if (
1870
+ this.pathMatchesTarget(
1871
+ session.treeEditingPath,
1872
+ targetPath,
1873
+ isDirectory
1874
+ )
1875
+ ) {
1876
+ session.treeEditingPath = '';
1877
+ }
1878
+
1879
+ if (
1880
+ this.pathMatchesTarget(
1881
+ session.pendingTreeFocusPath,
1882
+ targetPath,
1883
+ isDirectory
1884
+ )
1885
+ ) {
1886
+ session.pendingTreeFocusPath = '';
1887
+ }
1888
+
1889
+ if (
1890
+ this.pathMatchesTarget(
1891
+ session.pendingTreeRenameFocusPath,
1892
+ targetPath,
1893
+ isDirectory
1894
+ )
1895
+ ) {
1896
+ session.pendingTreeRenameFocusPath = '';
1897
+ }
1898
+
1899
+ const activeTabKey = session.workspaceState.activeTabKey || '';
1900
+ if (
1901
+ isFileWorkspaceTabKey(activeTabKey)
1902
+ && this.pathMatchesTarget(
1903
+ workspaceKeyToFilePath(activeTabKey),
1904
+ targetPath,
1905
+ isDirectory
1906
+ )
1907
+ ) {
1908
+ session.workspaceState.activeTabKey = '';
1909
+ visualChanged = true;
1910
+ }
1911
+
1912
+ const lastNonTerminal = session.workspaceState.lastNonTerminalTabKey || '';
1913
+ if (
1914
+ isFileWorkspaceTabKey(lastNonTerminal)
1915
+ && this.pathMatchesTarget(
1916
+ workspaceKeyToFilePath(lastNonTerminal),
1917
+ targetPath,
1918
+ isDirectory
1919
+ )
1920
+ ) {
1921
+ session.workspaceState.lastNonTerminalTabKey = '';
1922
+ }
1923
+
1924
+ return {
1925
+ workspaceChanged,
1926
+ visualChanged
1927
+ };
1928
+ }
1929
+
1930
+ focusTreePath(session, path) {
1931
+ if (!session?.fileTreeElement || !path) return;
1932
+ requestAnimationFrame(() => {
1933
+ const item = Array.from(
1934
+ session.fileTreeElement.querySelectorAll('li')
1935
+ ).find((candidate) => candidate.dataset.path === path);
1936
+ const row = item?.querySelector('.file-tree-item');
1937
+ if (row) {
1938
+ row.scrollIntoView({ block: 'nearest' });
1939
+ session.fileTreeElement.focus({ preventScroll: true });
1940
+ }
1941
+ });
1942
+ }
1943
+
1944
+ keepTreeFocus(session) {
1945
+ if (!session?.fileTreeElement || session.treeEditingPath) {
1946
+ return;
1947
+ }
1948
+ requestAnimationFrame(() => {
1949
+ if (!session?.fileTreeElement || session.treeEditingPath) {
1950
+ return;
1951
+ }
1952
+ session.fileTreeElement.focus({ preventScroll: true });
1953
+ });
1954
+ }
1955
+
1956
+ handleRenamedPaths(server, oldPath, newPath, isDirectory) {
1957
+ this.remapModelStorePaths(server, oldPath, newPath, isDirectory);
1958
+
1959
+ let currentSessionAffected = false;
1960
+ for (const session of state.sessions.values()) {
1961
+ if (session.serverId !== server.id) continue;
1962
+
1963
+ const { workspaceChanged, visualChanged } =
1964
+ this.applyRenamedPathToSession(
1965
+ session,
1966
+ oldPath,
1967
+ newPath,
1968
+ isDirectory
1969
+ );
1970
+ const pendingChanged = this.remapPendingFileWrites(
1971
+ session.key,
1972
+ oldPath,
1973
+ newPath,
1974
+ isDirectory
1975
+ );
1976
+
1977
+ if (workspaceChanged || pendingChanged) {
1978
+ session.saveState({ touchWorkspace: true });
1979
+ }
1980
+
1981
+ if (visualChanged && session.key === state.activeSessionKey) {
1982
+ currentSessionAffected = true;
1983
+ }
1984
+
1985
+ if (session.editorState.isVisible) {
1986
+ this.requestSessionTreeRefresh(session);
1987
+ }
1988
+ }
1989
+
1990
+ if (!currentSessionAffected || !this.currentSession) {
1991
+ return;
1992
+ }
1993
+
1994
+ this.renderEditorTabs();
1995
+ this.updateEditorPaneVisibility();
1996
+ const activeKey = this.getActiveWorkspaceTabKey(this.currentSession);
1997
+ if (isFileWorkspaceTabKey(activeKey)) {
1998
+ this.activateFileTab(
1999
+ workspaceKeyToFilePath(activeKey),
2000
+ true,
2001
+ { focusEditor: false }
2002
+ );
2003
+ return;
2004
+ }
2005
+ if (isAgentWorkspaceTabKey(activeKey)) {
2006
+ this.activateAgentTab(activeKey, true);
2007
+ return;
2008
+ }
2009
+ if (isTerminalWorkspaceTabKey(activeKey)) {
2010
+ this.activateTerminalTab(true);
2011
+ }
2012
+ }
2013
+
2014
+ handleDeletedPaths(server, targetPath, isDirectory) {
2015
+ this.removeDeletedModelStorePaths(server, targetPath, isDirectory);
2016
+
2017
+ let currentSessionAffected = false;
2018
+ for (const session of state.sessions.values()) {
2019
+ if (session.serverId !== server.id) continue;
2020
+
2021
+ const { workspaceChanged, visualChanged } =
2022
+ this.applyDeletedPathToSession(
2023
+ session,
2024
+ targetPath,
2025
+ isDirectory
2026
+ );
2027
+ const pendingChanged = this.removeDeletedPendingFileWrites(
2028
+ session.key,
2029
+ targetPath,
2030
+ isDirectory
2031
+ );
2032
+
2033
+ if (workspaceChanged || pendingChanged) {
2034
+ session.saveState({ touchWorkspace: true });
2035
+ }
2036
+
2037
+ if (visualChanged && session.key === state.activeSessionKey) {
2038
+ currentSessionAffected = true;
2039
+ }
2040
+
2041
+ if (session.editorState.isVisible) {
2042
+ this.requestSessionTreeRefresh(session);
2043
+ }
2044
+ }
2045
+
2046
+ if (!currentSessionAffected || !this.currentSession) {
2047
+ return;
2048
+ }
2049
+
2050
+ this.renderEditorTabs();
2051
+ this.updateEditorPaneVisibility();
2052
+ const activeKey = this.getActiveWorkspaceTabKey(this.currentSession);
2053
+ if (isFileWorkspaceTabKey(activeKey)) {
2054
+ this.activateFileTab(
2055
+ workspaceKeyToFilePath(activeKey),
2056
+ true,
2057
+ { focusEditor: false }
2058
+ );
2059
+ return;
2060
+ }
2061
+ if (isAgentWorkspaceTabKey(activeKey)) {
2062
+ this.activateAgentTab(activeKey, true);
2063
+ return;
2064
+ }
2065
+ if (isTerminalWorkspaceTabKey(activeKey)) {
2066
+ this.activateTerminalTab(true);
2067
+ return;
2068
+ }
2069
+ this.showEmptyState();
2070
+ }
2071
+
1458
2072
  async loadIconMap() {
1459
2073
  try {
1460
2074
  const res = await fetch('/icons/map.json');
@@ -1548,16 +2162,20 @@ class EditorManager {
1548
2162
  return !!session?.fileTreeElement && !!session?.editorState?.isVisible;
1549
2163
  }
1550
2164
 
2165
+ canRefreshSessionTree(session) {
2166
+ return this.isSessionTreeVisible(session) && !session.treeEditingPath;
2167
+ }
2168
+
1551
2169
  refreshVisibleSessionTrees() {
1552
2170
  for (const session of state.sessions.values()) {
1553
- if (this.isSessionTreeVisible(session)) {
2171
+ if (this.canRefreshSessionTree(session)) {
1554
2172
  this.requestSessionTreeRefresh(session);
1555
2173
  }
1556
2174
  }
1557
2175
  }
1558
2176
 
1559
- requestSessionTreeRefresh(session) {
1560
- if (!this.isSessionTreeVisible(session)) {
2177
+ requestSessionTreeRefresh(session, { force = false } = {}) {
2178
+ if (!force && !this.canRefreshSessionTree(session)) {
1561
2179
  this.updateTreeAutoRefresh();
1562
2180
  return;
1563
2181
  }
@@ -1565,7 +2183,7 @@ class EditorManager {
1565
2183
  session.fileTreeRefreshQueued = true;
1566
2184
  requestAnimationFrame(() => {
1567
2185
  session.fileTreeRefreshQueued = false;
1568
- if (this.isSessionTreeVisible(session)) {
2186
+ if (force || this.canRefreshSessionTree(session)) {
1569
2187
  this.refreshSessionTree(session);
1570
2188
  } else {
1571
2189
  this.updateTreeAutoRefresh();
@@ -1577,7 +2195,7 @@ class EditorManager {
1577
2195
  const shouldRun = (
1578
2196
  document.visibilityState === 'visible'
1579
2197
  && Array.from(state.sessions.values()).some(
1580
- (session) => this.isSessionTreeVisible(session)
2198
+ (session) => this.canRefreshSessionTree(session)
1581
2199
  )
1582
2200
  );
1583
2201
  if (shouldRun && !this.treeRefreshTimer) {
@@ -1586,20 +2204,365 @@ class EditorManager {
1586
2204
  this.updateTreeAutoRefresh();
1587
2205
  return;
1588
2206
  }
1589
- const hasVisibleTrees = Array.from(state.sessions.values()).some(
1590
- (session) => this.isSessionTreeVisible(session)
1591
- );
2207
+ const hasVisibleTrees = Array.from(
2208
+ state.sessions.values()
2209
+ ).some((session) => this.canRefreshSessionTree(session));
1592
2210
  if (!hasVisibleTrees) {
1593
2211
  this.updateTreeAutoRefresh();
1594
2212
  return;
1595
2213
  }
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;
2214
+ this.refreshVisibleSessionTrees();
2215
+ }, FILE_TREE_REFRESH_INTERVAL_MS);
2216
+ return;
2217
+ }
2218
+ if (!shouldRun && this.treeRefreshTimer) {
2219
+ window.clearInterval(this.treeRefreshTimer);
2220
+ this.treeRefreshTimer = null;
2221
+ }
2222
+ }
2223
+
2224
+ setSelectedTreePath(session, path, { preserveFocus = false } = {}) {
2225
+ if (!session) return;
2226
+ const nextPath = typeof path === 'string' ? path : '';
2227
+ if (session.selectedTreePath === nextPath) return;
2228
+ session.selectedTreePath = nextPath;
2229
+ if (preserveFocus && nextPath) {
2230
+ session.pendingTreeFocusPath = nextPath;
2231
+ }
2232
+ if (this.isSessionTreeVisible(session)) {
2233
+ this.syncSelectedTreePath(session);
2234
+ }
2235
+ }
2236
+
2237
+ syncSelectedTreePath(session) {
2238
+ if (!session?.fileTreeElement) return;
2239
+ const selectedPath = session.selectedTreePath || '';
2240
+ Array.from(
2241
+ session.fileTreeElement.querySelectorAll('.file-tree-item')
2242
+ ).forEach((row) => {
2243
+ const rowPath = row.parentElement?.dataset.path || '';
2244
+ row.classList.toggle(
2245
+ 'selected',
2246
+ selectedPath.length > 0 && rowPath === selectedPath
2247
+ );
2248
+ });
2249
+ }
2250
+
2251
+ getVisibleTreeRows(session) {
2252
+ if (!session?.fileTreeElement) return [];
2253
+ return Array.from(
2254
+ session.fileTreeElement.querySelectorAll('li > .file-tree-item')
2255
+ ).filter((row) => row instanceof HTMLElement);
2256
+ }
2257
+
2258
+ getDomSelectedTreePath(session) {
2259
+ return session?.fileTreeElement?.querySelector(
2260
+ '.file-tree-item.selected'
2261
+ )?.parentElement?.dataset.path || '';
2262
+ }
2263
+
2264
+ moveTreeSelection(session, delta) {
2265
+ if (!session || !delta) return false;
2266
+ const rows = this.getVisibleTreeRows(session);
2267
+ if (rows.length === 0) return false;
2268
+
2269
+ const currentPath = this.getDomSelectedTreePath(session)
2270
+ || session.selectedTreePath
2271
+ || session.editorState.activeFilePath
2272
+ || '';
2273
+ let currentIndex = rows.findIndex(
2274
+ (row) => row.parentElement?.dataset.path === currentPath
2275
+ );
2276
+ if (currentIndex === -1) {
2277
+ currentIndex = delta > 0 ? -1 : rows.length;
2278
+ }
2279
+
2280
+ const nextIndex = Math.max(
2281
+ 0,
2282
+ Math.min(rows.length - 1, currentIndex + delta)
2283
+ );
2284
+ const nextRow = rows[nextIndex];
2285
+ const nextPath = nextRow?.parentElement?.dataset.path || '';
2286
+ if (!nextPath) return false;
2287
+
2288
+ this.setSelectedTreePath(session, nextPath, { preserveFocus: true });
2289
+ nextRow.scrollIntoView({ block: 'nearest' });
2290
+ session.fileTreeElement?.focus({ preventScroll: true });
2291
+ return true;
2292
+ }
2293
+
2294
+ beginSelectedTreeRename(session) {
2295
+ if (!session) return false;
2296
+ const selectedPath = this.getDomSelectedTreePath(session)
2297
+ || session.selectedTreePath
2298
+ || '';
2299
+ if (!selectedPath) return false;
2300
+
2301
+ const item = session.fileTreeElement?.querySelector(
2302
+ `li[data-path="${CSS.escape(selectedPath)}"]`
2303
+ );
2304
+ const row = item?.querySelector('.file-tree-item');
2305
+ const nameEl = row?.querySelector('.file-tree-name');
2306
+ if (
2307
+ !item
2308
+ || !row
2309
+ || !nameEl
2310
+ || item.dataset.renameable !== '1'
2311
+ ) {
2312
+ return false;
2313
+ }
2314
+
2315
+ const renameButton = row.querySelector('.file-tree-rename-btn');
2316
+ if (
2317
+ renameButton instanceof HTMLButtonElement
2318
+ && !renameButton.disabled
2319
+ ) {
2320
+ renameButton.click();
2321
+ return true;
2322
+ }
2323
+
2324
+ this.beginTreeRename(session, {
2325
+ path: selectedPath,
2326
+ name: nameEl.textContent || '',
2327
+ isDirectory: item.dataset.isDirectory === '1',
2328
+ renameable: true
2329
+ });
2330
+ return true;
2331
+ }
2332
+
2333
+ async deleteSelectedTreeEntry(session) {
2334
+ if (!session) return false;
2335
+ const selectedPath = this.getDomSelectedTreePath(session)
2336
+ || session.selectedTreePath
2337
+ || '';
2338
+ if (!selectedPath) return false;
2339
+
2340
+ const item = session.fileTreeElement?.querySelector(
2341
+ `li[data-path="${CSS.escape(selectedPath)}"]`
2342
+ );
2343
+ const row = item?.querySelector('.file-tree-item');
2344
+ const nameEl = row?.querySelector('.file-tree-name');
2345
+ if (
2346
+ !item
2347
+ || !row
2348
+ || !nameEl
2349
+ || item.dataset.deleteable !== '1'
2350
+ ) {
2351
+ return false;
2352
+ }
2353
+
2354
+ await this.deleteTreeEntry(session, {
2355
+ path: selectedPath,
2356
+ name: nameEl.textContent || '',
2357
+ isDirectory: item.dataset.isDirectory === '1',
2358
+ deleteable: true
2359
+ });
2360
+ return true;
2361
+ }
2362
+
2363
+ async createTreeEntry(session, parentPath, kind) {
2364
+ if (!session || typeof parentPath !== 'string' || !parentPath) {
2365
+ return;
2366
+ }
2367
+
2368
+ try {
2369
+ const response = await session.server.fetch('/api/fs/create', {
2370
+ method: 'POST',
2371
+ headers: { 'Content-Type': 'application/json' },
2372
+ body: JSON.stringify({
2373
+ parentPath,
2374
+ kind
2375
+ })
2376
+ });
2377
+ if (!response.ok) {
2378
+ await throwResponseError(response, 'Failed to create path');
2379
+ }
2380
+
2381
+ const payload = await response.json();
2382
+ if (
2383
+ parentPath !== '.'
2384
+ && !session.sharedWorkspaceState.expandedPaths.includes(parentPath)
2385
+ ) {
2386
+ session.sharedWorkspaceState.expandedPaths =
2387
+ uniqueStringList([
2388
+ ...session.sharedWorkspaceState.expandedPaths,
2389
+ parentPath
2390
+ ]);
2391
+ session.saveState({ touchWorkspace: true });
2392
+ void session.server.fetch('/api/memory/expand', {
2393
+ method: 'POST',
2394
+ headers: { 'Content-Type': 'application/json' },
2395
+ body: JSON.stringify({
2396
+ path: parentPath,
2397
+ expanded: true
2398
+ })
2399
+ });
2400
+ }
2401
+
2402
+ this.beginTreeRename(session, {
2403
+ path: payload.path,
2404
+ name: payload.name,
2405
+ isDirectory: !!payload.isDirectory,
2406
+ renameable: true
2407
+ });
2408
+ } catch (error) {
2409
+ alert(error.message || 'Failed to create path', {
2410
+ type: 'error',
2411
+ title: 'Files'
2412
+ });
2413
+ }
2414
+ }
2415
+
2416
+ cancelTreeRename(session) {
2417
+ if (!session || !session.treeEditingPath) return;
2418
+ session.treeEditingPath = '';
2419
+ session.treeRenameSubmitting = false;
2420
+ session.pendingTreeRenameFocusPath = '';
2421
+ if (this.isSessionTreeVisible(session)) {
2422
+ this.requestSessionTreeRefresh(session);
2423
+ }
2424
+ }
2425
+
2426
+ beginTreeRename(session, file) {
2427
+ if (!session || !file?.renameable) return;
2428
+ session.selectedTreePath = file.path;
2429
+ session.pendingTreeFocusPath = '';
2430
+ session.treeEditingPath = file.path;
2431
+ session.treeRenameSubmitting = false;
2432
+ session.pendingTreeRenameFocusPath = file.path;
2433
+ this.requestSessionTreeRefresh(session, { force: true });
2434
+ }
2435
+
2436
+ async deleteTreeEntry(session, file) {
2437
+ if (!session || !file?.deleteable) {
2438
+ return;
2439
+ }
2440
+ const confirmed = await showConfirmModal({
2441
+ title: file.isDirectory
2442
+ ? '⚠️ Delete Folder'
2443
+ : '⚠️ Delete File',
2444
+ message: file.isDirectory
2445
+ ? `Delete folder "${file.name}" and all of its contents?`
2446
+ : `Delete file "${file.name}"?`,
2447
+ note: 'ℹ️ Deleted items do not go to the Trash.',
2448
+ confirmLabel: 'Delete',
2449
+ danger: true,
2450
+ returnFocus: session.fileTreeElement
2451
+ });
2452
+ if (!confirmed) {
2453
+ session.fileTreeElement?.focus({ preventScroll: true });
2454
+ return;
2455
+ }
2456
+
2457
+ try {
2458
+ const response = await session.server.fetch('/api/fs/delete', {
2459
+ method: 'POST',
2460
+ headers: { 'Content-Type': 'application/json' },
2461
+ body: JSON.stringify({
2462
+ path: file.path
2463
+ })
2464
+ });
2465
+ if (!response.ok) {
2466
+ await throwResponseError(response, 'Failed to delete path');
2467
+ }
2468
+ const payload = await response.json();
2469
+ session.selectedTreePath = '';
2470
+ session.pendingTreeFocusPath = '';
2471
+ session.pendingTreeRenameFocusPath = '';
2472
+ session.treeEditingPath = '';
2473
+ this.handleDeletedPaths(
2474
+ session.server,
2475
+ payload.path || file.path,
2476
+ !!payload.isDirectory
2477
+ );
2478
+ this.requestSessionTreeRefresh(session);
2479
+ session.fileTreeElement?.focus({ preventScroll: true });
2480
+ } catch (error) {
2481
+ alert(error.message || 'Failed to delete path', {
2482
+ type: 'error',
2483
+ title: 'Files'
2484
+ });
2485
+ }
2486
+ }
2487
+
2488
+ async commitTreeRename(session, file, nextName) {
2489
+ if (!session || !file || typeof nextName !== 'string') {
2490
+ return;
2491
+ }
2492
+ if (nextName.length === 0) {
2493
+ return;
2494
+ }
2495
+ if (nextName === file.name) {
2496
+ this.cancelTreeRename(session);
2497
+ this.focusTreePath(session, file.path);
2498
+ return;
2499
+ }
2500
+
2501
+ session.treeRenameSubmitting = true;
2502
+ try {
2503
+ const response = await session.server.fetch('/api/fs/rename', {
2504
+ method: 'POST',
2505
+ headers: { 'Content-Type': 'application/json' },
2506
+ body: JSON.stringify({
2507
+ path: file.path,
2508
+ newName: nextName
2509
+ })
2510
+ });
2511
+ if (!response.ok) {
2512
+ if (response.status === 409) {
2513
+ let message = 'A file or folder with that name already exists.';
2514
+ try {
2515
+ const payload = await response.json();
2516
+ if (payload?.error) {
2517
+ message = payload.error;
2518
+ }
2519
+ } catch {
2520
+ // Ignore invalid JSON error bodies.
2521
+ }
2522
+ await showConfirmModal({
2523
+ title: 'Rename Failed',
2524
+ message,
2525
+ confirmLabel: 'OK',
2526
+ hideCancel: true
2527
+ });
2528
+ session.treeRenameSubmitting = false;
2529
+ requestAnimationFrame(() => {
2530
+ const renameInput = session.fileTreeElement?.querySelector(
2531
+ '.file-tree-rename-input'
2532
+ );
2533
+ if (renameInput instanceof HTMLInputElement) {
2534
+ renameInput.focus({ preventScroll: true });
2535
+ renameInput.setSelectionRange(
2536
+ 0,
2537
+ renameInput.value.length
2538
+ );
2539
+ }
2540
+ });
2541
+ return;
2542
+ }
2543
+ await throwResponseError(response, 'Failed to rename path');
2544
+ }
2545
+ const payload = await response.json();
2546
+ session.treeEditingPath = '';
2547
+ session.treeRenameSubmitting = false;
2548
+ session.pendingTreeRenameFocusPath = '';
2549
+ session.selectedTreePath = payload.newPath || file.path;
2550
+ session.pendingTreeFocusPath = payload.newPath || file.path;
2551
+ this.handleRenamedPaths(
2552
+ session.server,
2553
+ file.path,
2554
+ payload.newPath || file.path,
2555
+ !!payload.isDirectory
2556
+ );
2557
+ this.requestSessionTreeRefresh(session);
2558
+ this.focusTreePath(session, session.pendingTreeFocusPath);
2559
+ } catch (error) {
2560
+ session.treeRenameSubmitting = false;
2561
+ this.cancelTreeRename(session);
2562
+ alert(error.message || 'Failed to rename path', {
2563
+ type: 'error',
2564
+ title: 'Files'
2565
+ });
1603
2566
  }
1604
2567
  }
1605
2568
 
@@ -1622,9 +2585,78 @@ class EditorManager {
1622
2585
  return session.sharedWorkspaceState.expandedPaths.includes(filePath);
1623
2586
  }
1624
2587
 
2588
+ updateTreeCreateRow(list, dirPath, creatable, session) {
2589
+ let row = Array.from(list.children).find(
2590
+ (child) => child.classList?.contains('file-tree-create-entry')
2591
+ );
2592
+
2593
+ if (!creatable) {
2594
+ row?.remove();
2595
+ return;
2596
+ }
2597
+
2598
+ if (!row) {
2599
+ row = document.createElement('li');
2600
+ row.className = 'file-tree-create-entry';
2601
+
2602
+ const actions = document.createElement('div');
2603
+ actions.className = 'file-tree-create-actions';
2604
+
2605
+ const newFolderButton = document.createElement('button');
2606
+ newFolderButton.type = 'button';
2607
+ newFolderButton.className = 'file-tree-new-folder-btn';
2608
+ newFolderButton.title = 'New Folder';
2609
+ newFolderButton.innerHTML = NEW_FOLDER_ICON_SVG;
2610
+ actions.appendChild(newFolderButton);
2611
+
2612
+ const newFileButton = document.createElement('button');
2613
+ newFileButton.type = 'button';
2614
+ newFileButton.className = 'file-tree-new-file-btn';
2615
+ newFileButton.title = 'New File';
2616
+ newFileButton.innerHTML = NEW_FILE_ICON_SVG;
2617
+ actions.appendChild(newFileButton);
2618
+
2619
+ row.appendChild(actions);
2620
+ }
2621
+
2622
+ const newFolderButton = row.querySelector('.file-tree-new-folder-btn');
2623
+ const newFileButton = row.querySelector('.file-tree-new-file-btn');
2624
+
2625
+ if (newFolderButton instanceof HTMLButtonElement) {
2626
+ newFolderButton.setAttribute(
2627
+ 'aria-label',
2628
+ `New folder in ${dirPath}`
2629
+ );
2630
+ newFolderButton.onmousedown = (event) => {
2631
+ event.preventDefault();
2632
+ event.stopPropagation();
2633
+ };
2634
+ newFolderButton.onclick = (event) => {
2635
+ event.stopPropagation();
2636
+ void this.createTreeEntry(session, dirPath, 'directory');
2637
+ };
2638
+ }
2639
+
2640
+ if (newFileButton instanceof HTMLButtonElement) {
2641
+ newFileButton.setAttribute('aria-label', `New file in ${dirPath}`);
2642
+ newFileButton.onmousedown = (event) => {
2643
+ event.preventDefault();
2644
+ event.stopPropagation();
2645
+ };
2646
+ newFileButton.onclick = (event) => {
2647
+ event.stopPropagation();
2648
+ void this.createTreeEntry(session, dirPath, 'file');
2649
+ };
2650
+ }
2651
+
2652
+ list.appendChild(row);
2653
+ }
2654
+
1625
2655
  updateTreeItem(li, file, session, renderToken) {
1626
2656
  li.dataset.path = file.path;
1627
2657
  li.dataset.isDirectory = file.isDirectory ? '1' : '0';
2658
+ li.dataset.renameable = file.renameable ? '1' : '0';
2659
+ li.dataset.deleteable = file.deleteable ? '1' : '0';
1628
2660
 
1629
2661
  let row = Array.from(li.children).find(
1630
2662
  (child) => child.classList?.contains('file-tree-item')
@@ -1634,6 +2666,7 @@ class EditorManager {
1634
2666
  row.className = 'file-tree-item';
1635
2667
  li.prepend(row);
1636
2668
  }
2669
+ row.tabIndex = -1;
1637
2670
 
1638
2671
  let icon = row.querySelector('.icon');
1639
2672
  if (!icon) {
@@ -1642,14 +2675,47 @@ class EditorManager {
1642
2675
  row.appendChild(icon);
1643
2676
  }
1644
2677
 
1645
- let name = Array.from(row.children).find(
1646
- (child) => child !== icon
1647
- );
2678
+ let renameButton = row.querySelector('.file-tree-rename-btn');
2679
+ if (!renameButton) {
2680
+ renameButton = document.createElement('button');
2681
+ renameButton.type = 'button';
2682
+ renameButton.className = 'file-tree-rename-btn';
2683
+ renameButton.title = 'Rename';
2684
+ renameButton.setAttribute('aria-label', `Rename ${file.name}`);
2685
+ renameButton.innerHTML = RENAME_ICON_SVG;
2686
+ row.appendChild(renameButton);
2687
+ }
2688
+
2689
+ let deleteButton = row.querySelector('.file-tree-delete-btn');
2690
+ if (!deleteButton) {
2691
+ deleteButton = document.createElement('button');
2692
+ deleteButton.type = 'button';
2693
+ deleteButton.className = 'file-tree-delete-btn';
2694
+ deleteButton.title = 'Delete';
2695
+ deleteButton.setAttribute('aria-label', `Delete ${file.name}`);
2696
+ deleteButton.innerHTML = DELETE_ICON_SVG;
2697
+ row.appendChild(deleteButton);
2698
+ }
2699
+
2700
+ let name = row.querySelector('.file-tree-name');
1648
2701
  if (!name) {
1649
2702
  name = document.createElement('span');
2703
+ name.className = 'file-tree-name';
1650
2704
  row.appendChild(name);
1651
2705
  }
1652
2706
 
2707
+ let renameInput = row.querySelector('.file-tree-rename-input');
2708
+ const isEditing = session.treeEditingPath === file.path;
2709
+ if (isEditing && !renameInput) {
2710
+ renameInput = document.createElement('input');
2711
+ renameInput.type = 'text';
2712
+ renameInput.className = 'file-tree-rename-input';
2713
+ row.appendChild(renameInput);
2714
+ } else if (!isEditing && renameInput) {
2715
+ renameInput.remove();
2716
+ renameInput = null;
2717
+ }
2718
+
1653
2719
  row.className = 'file-tree-item';
1654
2720
  if (file.isDirectory) {
1655
2721
  row.classList.add('is-dir');
@@ -1659,15 +2725,110 @@ class EditorManager {
1659
2725
  !file.isDirectory
1660
2726
  && session.editorState.activeFilePath === file.path
1661
2727
  );
2728
+ row.classList.toggle(
2729
+ 'selected',
2730
+ session.selectedTreePath === file.path
2731
+ );
2732
+ row.classList.toggle('editing', isEditing);
1662
2733
 
1663
2734
  const isExpanded = file.isDirectory
1664
2735
  && this.getTreeItemExpanded(file.path, session);
1665
2736
  li.classList.toggle('expanded', isExpanded);
1666
2737
  icon.innerHTML = this.getIcon(file.name, file.isDirectory, isExpanded);
1667
2738
  name.textContent = file.name;
2739
+ name.style.display = isEditing ? 'none' : '';
2740
+ renameButton.style.display = isEditing ? 'none' : '';
2741
+ deleteButton.style.display = isEditing ? 'none' : '';
2742
+ renameButton.hidden = !file.renameable;
2743
+ renameButton.disabled = !file.renameable;
2744
+ renameButton.title = `Rename ${file.name}`;
2745
+ renameButton.setAttribute('aria-label', `Rename ${file.name}`);
2746
+ renameButton.onmousedown = (event) => {
2747
+ event.preventDefault();
2748
+ event.stopPropagation();
2749
+ };
2750
+ renameButton.onclick = (event) => {
2751
+ event.stopPropagation();
2752
+ this.beginTreeRename(session, file);
2753
+ };
2754
+
2755
+ deleteButton.hidden = !file.deleteable;
2756
+ deleteButton.disabled = !file.deleteable;
2757
+ deleteButton.title = `Delete ${file.name}`;
2758
+ deleteButton.setAttribute('aria-label', `Delete ${file.name}`);
2759
+ deleteButton.onmousedown = (event) => {
2760
+ event.preventDefault();
2761
+ event.stopPropagation();
2762
+ };
2763
+ deleteButton.onclick = (event) => {
2764
+ event.stopPropagation();
2765
+ void this.deleteTreeEntry(session, file);
2766
+ };
2767
+
2768
+ if (renameInput) {
2769
+ if (document.activeElement !== renameInput) {
2770
+ renameInput.value = file.name;
2771
+ }
2772
+ renameInput.onkeydown = async (event) => {
2773
+ if (event.key === 'Escape') {
2774
+ event.preventDefault();
2775
+ event.stopPropagation();
2776
+ this.cancelTreeRename(session);
2777
+ this.focusTreePath(session, file.path);
2778
+ return;
2779
+ }
2780
+ if (event.key === 'Enter') {
2781
+ event.preventDefault();
2782
+ event.stopPropagation();
2783
+ await this.commitTreeRename(
2784
+ session,
2785
+ file,
2786
+ renameInput.value
2787
+ );
2788
+ }
2789
+ };
2790
+ renameInput.onmousedown = (event) => {
2791
+ event.stopPropagation();
2792
+ };
2793
+ renameInput.onclick = (event) => {
2794
+ event.stopPropagation();
2795
+ };
2796
+ renameInput.onfocus = (event) => {
2797
+ event.stopPropagation();
2798
+ };
2799
+ renameInput.onblur = () => {
2800
+ if (!session.treeRenameSubmitting) {
2801
+ this.cancelTreeRename(session);
2802
+ }
2803
+ };
2804
+
2805
+ if (session.pendingTreeRenameFocusPath === file.path) {
2806
+ session.pendingTreeRenameFocusPath = '';
2807
+ requestAnimationFrame(() => {
2808
+ renameInput.focus({ preventScroll: true });
2809
+ renameInput.setSelectionRange(
2810
+ 0,
2811
+ renameInput.value.length
2812
+ );
2813
+ });
2814
+ }
2815
+ }
1668
2816
 
1669
2817
  row.onclick = async (e) => {
1670
2818
  e.stopPropagation();
2819
+ if (e.target.closest('.file-tree-rename-btn')) {
2820
+ return;
2821
+ }
2822
+ if (e.target.closest('.file-tree-delete-btn')) {
2823
+ return;
2824
+ }
2825
+ if (e.target.closest('.file-tree-rename-input')) {
2826
+ return;
2827
+ }
2828
+ this.setSelectedTreePath(session, file.path, {
2829
+ preserveFocus: true
2830
+ });
2831
+ session.fileTreeElement?.focus({ preventScroll: true });
1671
2832
  if (file.isDirectory) {
1672
2833
  if (li.classList.contains('expanded')) {
1673
2834
  li.classList.remove('expanded');
@@ -1711,22 +2872,49 @@ class EditorManager {
1711
2872
  icon.innerHTML = this.getIcon(file.name, true, true);
1712
2873
  await this.renderTree(file.path, li, session, renderToken);
1713
2874
  this.updateTreeAutoRefresh();
2875
+ session.fileTreeElement?.focus({ preventScroll: true });
1714
2876
  return;
1715
2877
  }
1716
2878
 
1717
- await this.openFile(file.path, session);
2879
+ await this.openFile(file.path, session, {
2880
+ focusEditor: false
2881
+ });
2882
+ this.focusTreePath(session, file.path);
2883
+ session.pendingTreeFocusPath = file.path;
1718
2884
  this.requestSessionTreeRefresh(session);
1719
2885
  };
1720
2886
 
2887
+ row.onmousedown = (event) => {
2888
+ if (
2889
+ event.target.closest('.file-tree-rename-btn')
2890
+ || event.target.closest('.file-tree-delete-btn')
2891
+ || event.target.closest('.file-tree-rename-input')
2892
+ ) {
2893
+ return;
2894
+ }
2895
+ event.preventDefault();
2896
+ session.fileTreeElement?.focus({ preventScroll: true });
2897
+ };
2898
+
2899
+ row.onkeydown = null;
2900
+
1721
2901
  if (!isExpanded) {
1722
2902
  const childUl = this.getTreeChildList(li);
1723
2903
  if (childUl) {
1724
2904
  childUl.remove();
1725
2905
  }
1726
2906
  }
2907
+
2908
+ if (session.pendingTreeFocusPath === file.path) {
2909
+ session.pendingTreeFocusPath = '';
2910
+ requestAnimationFrame(() => {
2911
+ row.scrollIntoView({ block: 'nearest' });
2912
+ session.fileTreeElement?.focus({ preventScroll: true });
2913
+ });
2914
+ }
1727
2915
  }
1728
2916
 
1729
- reconcileTreeList(list, files, session, renderToken) {
2917
+ reconcileTreeList(list, dirPath, files, creatable, session, renderToken) {
1730
2918
  const existingItems = new Map();
1731
2919
  Array.from(list.children).forEach((child) => {
1732
2920
  if (child.tagName === 'LI' && child.dataset.path) {
@@ -1753,6 +2941,8 @@ class EditorManager {
1753
2941
  for (const li of orderedItems) {
1754
2942
  list.appendChild(li);
1755
2943
  }
2944
+
2945
+ this.updateTreeCreateRow(list, dirPath, creatable, session);
1756
2946
  }
1757
2947
 
1758
2948
  initMonaco() {
@@ -1960,11 +3150,26 @@ class EditorManager {
1960
3150
  `/api/fs/list?path=${encodeURIComponent(dirPath)}`
1961
3151
  );
1962
3152
  if (!res.ok) return;
1963
- const files = await res.json();
3153
+ const payload = await res.json();
3154
+ const files = Array.isArray(payload)
3155
+ ? payload
3156
+ : Array.isArray(payload?.items)
3157
+ ? payload.items
3158
+ : [];
3159
+ const creatable = Array.isArray(payload)
3160
+ ? false
3161
+ : !!payload?.creatable;
1964
3162
  if ((session.fileTreeRenderToken || 0) !== renderToken) return;
1965
3163
 
1966
3164
  const list = this.ensureTreeList(container);
1967
- this.reconcileTreeList(list, files, session, renderToken);
3165
+ this.reconcileTreeList(
3166
+ list,
3167
+ dirPath,
3168
+ files,
3169
+ creatable,
3170
+ session,
3171
+ renderToken
3172
+ );
1968
3173
  if ((session.fileTreeRenderToken || 0) !== renderToken) return;
1969
3174
 
1970
3175
  for (const file of files) {
@@ -1985,7 +3190,11 @@ class EditorManager {
1985
3190
  }
1986
3191
  }
1987
3192
 
1988
- async openFile(filePath, sessionOrRestore = this.currentSession) {
3193
+ async openFile(
3194
+ filePath,
3195
+ sessionOrRestore = this.currentSession,
3196
+ options = {}
3197
+ ) {
1989
3198
  const session = typeof sessionOrRestore === 'boolean'
1990
3199
  ? this.currentSession
1991
3200
  : sessionOrRestore;
@@ -1998,20 +3207,10 @@ class EditorManager {
1998
3207
  : session;
1999
3208
  if (!targetSession) return;
2000
3209
  const state = targetSession.editorState;
2001
-
2002
- let touchedWorkspace = false;
2003
- if (!state.openFiles.includes(filePath)) {
2004
- state.openFiles.push(filePath);
2005
- this.renderEditorTabs();
2006
- touchedWorkspace = true;
2007
- }
2008
-
2009
- this.updateEditorPaneVisibility();
3210
+ const wasOpen = state.openFiles.includes(filePath);
3211
+ const isImage = isSupportedImagePath(filePath);
2010
3212
 
2011
3213
  if (!this.getModel(filePath)) {
2012
- const ext = filePath.split('.').pop().toLowerCase();
2013
- const isImage = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext);
2014
-
2015
3214
  let model = null;
2016
3215
  let content = null;
2017
3216
  let readonly = false;
@@ -2021,7 +3220,20 @@ class EditorManager {
2021
3220
  const res = await targetSession.server.fetch(
2022
3221
  `/api/fs/read?path=${encodeURIComponent(filePath)}`
2023
3222
  );
2024
- if (!res.ok) throw new Error('Failed to read file');
3223
+ if (res.status === 415) {
3224
+ await showConfirmModal({
3225
+ title: 'Unsupported File Type',
3226
+ message: 'This file type is not supported yet.',
3227
+ note: 'Only text files and supported images can be opened right now.',
3228
+ confirmLabel: 'OK',
3229
+ hideCancel: true,
3230
+ returnFocus: document.activeElement
3231
+ });
3232
+ return;
3233
+ }
3234
+ if (!res.ok) {
3235
+ throw new Error('Failed to read file');
3236
+ }
2025
3237
  const data = await res.json();
2026
3238
  content = data.content;
2027
3239
  readonly = data.readonly;
@@ -2051,7 +3263,16 @@ class EditorManager {
2051
3263
  });
2052
3264
  }
2053
3265
 
2054
- this.activateFileTab(filePath);
3266
+ let touchedWorkspace = false;
3267
+ if (!wasOpen) {
3268
+ state.openFiles.push(filePath);
3269
+ this.renderEditorTabs();
3270
+ touchedWorkspace = true;
3271
+ }
3272
+
3273
+ this.updateEditorPaneVisibility();
3274
+
3275
+ this.activateFileTab(filePath, false, options);
2055
3276
  if (touchedWorkspace) {
2056
3277
  targetSession.saveState({ touchWorkspace: true });
2057
3278
  }
@@ -2258,9 +3479,10 @@ class EditorManager {
2258
3479
  });
2259
3480
  }
2260
3481
 
2261
- activateFileTab(filePath, isRestore = false) {
3482
+ activateFileTab(filePath, isRestore = false, options = {}) {
2262
3483
  if (!this.currentSession) return;
2263
3484
  if (!filePath) return;
3485
+ const focusEditor = options.focusEditor !== false;
2264
3486
  const state = this.currentSession.editorState;
2265
3487
 
2266
3488
  if (!isRestore && state.activeFilePath && state.activeFilePath !== filePath) {
@@ -2282,7 +3504,7 @@ class EditorManager {
2282
3504
  this.syncTerminalWorkspacePlacement();
2283
3505
 
2284
3506
  if (!file) {
2285
- this.openFile(filePath, true);
3507
+ this.openFile(filePath, true, options);
2286
3508
  return;
2287
3509
  }
2288
3510
 
@@ -2317,7 +3539,9 @@ class EditorManager {
2317
3539
  if (savedViewState) {
2318
3540
  this.editor.restoreViewState(savedViewState);
2319
3541
  }
2320
- this.editor.focus();
3542
+ if (focusEditor) {
3543
+ this.editor.focus();
3544
+ }
2321
3545
  // Force layout to ensure content is visible
2322
3546
  requestAnimationFrame(() => this.editor.layout());
2323
3547
  }
@@ -4811,6 +6035,11 @@ class Session {
4811
6035
  this.layoutState = {
4812
6036
  editorFlex: '2 1 0%'
4813
6037
  };
6038
+ this.selectedTreePath = '';
6039
+ this.treeEditingPath = '';
6040
+ this.treeRenameSubmitting = false;
6041
+ this.pendingTreeFocusPath = '';
6042
+ this.pendingTreeRenameFocusPath = '';
4814
6043
  this.previewRelayoutScheduled = false;
4815
6044
  this.lastTerminalControlClaimAt = 0;
4816
6045
  this.boundTerminalClaimRoot = null;
@@ -11052,7 +12281,66 @@ function createTabElement(session) {
11052
12281
 
11053
12282
  const fileTree = document.createElement('div');
11054
12283
  fileTree.className = 'tab-file-tree';
12284
+ fileTree.tabIndex = 0;
11055
12285
  session.fileTreeElement = fileTree;
12286
+ fileTree.addEventListener('mousedown', (event) => {
12287
+ if (
12288
+ event.target.closest('.file-tree-rename-input')
12289
+ || event.target.closest('.file-tree-rename-btn')
12290
+ ) {
12291
+ return;
12292
+ }
12293
+ if (event.target.closest('.file-tree-item')) {
12294
+ event.preventDefault();
12295
+ fileTree.focus({ preventScroll: true });
12296
+ }
12297
+ });
12298
+ fileTree.addEventListener('keydown', (event) => {
12299
+ if (event.key === 'Escape' && session.treeEditingPath) {
12300
+ event.preventDefault();
12301
+ event.stopPropagation();
12302
+ editorManager.cancelTreeRename(session);
12303
+ editorManager.focusTreePath(session, session.selectedTreePath);
12304
+ return;
12305
+ }
12306
+ if (
12307
+ !session.treeEditingPath
12308
+ && !event.metaKey
12309
+ && !event.ctrlKey
12310
+ && !event.altKey
12311
+ && (
12312
+ event.key === 'Delete'
12313
+ || event.key === 'Backspace'
12314
+ )
12315
+ ) {
12316
+ event.preventDefault();
12317
+ event.stopPropagation();
12318
+ void editorManager.deleteSelectedTreeEntry(session);
12319
+ return;
12320
+ }
12321
+ if (event.key === 'ArrowDown') {
12322
+ event.preventDefault();
12323
+ event.stopPropagation();
12324
+ editorManager.moveTreeSelection(session, 1);
12325
+ editorManager.keepTreeFocus(session);
12326
+ return;
12327
+ }
12328
+ if (event.key === 'ArrowUp') {
12329
+ event.preventDefault();
12330
+ event.stopPropagation();
12331
+ editorManager.moveTreeSelection(session, -1);
12332
+ editorManager.keepTreeFocus(session);
12333
+ return;
12334
+ }
12335
+ if (event.key !== 'Enter' || session.treeEditingPath) {
12336
+ return;
12337
+ }
12338
+ if (!editorManager.beginSelectedTreeRename(session)) {
12339
+ return;
12340
+ }
12341
+ event.preventDefault();
12342
+ event.stopPropagation();
12343
+ });
11056
12344
 
11057
12345
  if (session.editorState && session.editorState.isVisible) {
11058
12346
  editorManager.refreshSessionTree(session);
@@ -11269,6 +12557,127 @@ function openServerModal(mode, server = null) {
11269
12557
  return true;
11270
12558
  }
11271
12559
 
12560
+ const confirmModalState = {
12561
+ resolve: null,
12562
+ returnFocus: null,
12563
+ preferredFocus: 'confirm',
12564
+ hideCancel: false
12565
+ };
12566
+
12567
+ function isConfirmModalOpen() {
12568
+ return !!confirmModal && confirmModal.style.display !== 'none';
12569
+ }
12570
+
12571
+ function getVisibleConfirmModalButtons() {
12572
+ const buttons = [];
12573
+ if (confirmModalCancel && !confirmModalState.hideCancel) {
12574
+ buttons.push(confirmModalCancel);
12575
+ }
12576
+ if (confirmModalConfirm) {
12577
+ buttons.push(confirmModalConfirm);
12578
+ }
12579
+ return buttons;
12580
+ }
12581
+
12582
+ function getConfirmModalPreferredButton() {
12583
+ if (!confirmModalConfirm) {
12584
+ return null;
12585
+ }
12586
+ if (confirmModalState.hideCancel || !confirmModalCancel) {
12587
+ return confirmModalConfirm;
12588
+ }
12589
+ return confirmModalState.preferredFocus === 'cancel'
12590
+ ? confirmModalCancel
12591
+ : confirmModalConfirm;
12592
+ }
12593
+
12594
+ function settleConfirmModal(result) {
12595
+ if (!confirmModal) return;
12596
+ confirmModal.style.display = 'none';
12597
+ const resolve = confirmModalState.resolve;
12598
+ const returnFocus = confirmModalState.returnFocus;
12599
+ confirmModalState.resolve = null;
12600
+ confirmModalState.returnFocus = null;
12601
+ confirmModalState.preferredFocus = 'confirm';
12602
+ confirmModalState.hideCancel = false;
12603
+ if (returnFocus instanceof HTMLElement) {
12604
+ requestAnimationFrame(() => {
12605
+ try {
12606
+ returnFocus.focus({ preventScroll: true });
12607
+ } catch {
12608
+ // Ignore focus restoration failures.
12609
+ }
12610
+ });
12611
+ }
12612
+ resolve?.(result);
12613
+ }
12614
+
12615
+ function showConfirmModal({
12616
+ title = 'Confirm',
12617
+ message = '',
12618
+ note = '',
12619
+ confirmLabel = 'Confirm',
12620
+ danger = false,
12621
+ hideCancel = false,
12622
+ returnFocus = null
12623
+ } = {}) {
12624
+ if (
12625
+ !confirmModal
12626
+ || !confirmModalTitle
12627
+ || !confirmModalMessage
12628
+ || !confirmModalNote
12629
+ || !confirmModalConfirm
12630
+ || !confirmModalCancel
12631
+ ) {
12632
+ return Promise.resolve(false);
12633
+ }
12634
+ if (confirmModalState.resolve) {
12635
+ settleConfirmModal(false);
12636
+ }
12637
+ confirmModalTitle.textContent = title;
12638
+ confirmModalMessage.textContent = message;
12639
+ confirmModalNote.textContent = note;
12640
+ confirmModalNote.style.display = note ? '' : 'none';
12641
+ confirmModalCancel.style.display = hideCancel ? 'none' : '';
12642
+ confirmModalConfirm.textContent = confirmLabel;
12643
+ confirmModalConfirm.classList.toggle('danger-button', danger);
12644
+ confirmModal.style.display = 'flex';
12645
+ confirmModalState.returnFocus = returnFocus;
12646
+ confirmModalState.hideCancel = hideCancel;
12647
+ confirmModalState.preferredFocus = 'confirm';
12648
+ requestAnimationFrame(() => {
12649
+ getConfirmModalPreferredButton()?.focus({ preventScroll: true });
12650
+ });
12651
+ return new Promise((resolve) => {
12652
+ confirmModalState.resolve = resolve;
12653
+ });
12654
+ }
12655
+
12656
+ function moveConfirmModalFocus(delta) {
12657
+ const buttons = getVisibleConfirmModalButtons();
12658
+ if (!buttons.length || !delta) {
12659
+ return;
12660
+ }
12661
+ if (buttons.length === 1) {
12662
+ buttons[0].focus({ preventScroll: true });
12663
+ return;
12664
+ }
12665
+ const currentIndex = buttons.findIndex(
12666
+ (button) => button === document.activeElement
12667
+ );
12668
+ const baseIndex = currentIndex === -1
12669
+ ? buttons.length - 1
12670
+ : currentIndex;
12671
+ const nextIndex = Math.max(0, Math.min(
12672
+ buttons.length - 1,
12673
+ baseIndex + delta
12674
+ ));
12675
+ confirmModalState.preferredFocus = nextIndex === 0
12676
+ ? 'cancel'
12677
+ : 'confirm';
12678
+ buttons[nextIndex].focus({ preventScroll: true });
12679
+ }
12680
+
11272
12681
  function renderServerControls() {
11273
12682
  if (!serverControlsEl) return;
11274
12683
  serverControlsEl.innerHTML = '';
@@ -11839,6 +13248,84 @@ if (
11839
13248
  });
11840
13249
  }
11841
13250
 
13251
+ if (
13252
+ confirmModal
13253
+ && confirmModalCancel
13254
+ && confirmModalConfirm
13255
+ ) {
13256
+ const focusPreferredConfirmButton = () => {
13257
+ requestAnimationFrame(() => {
13258
+ if (!isConfirmModalOpen()) return;
13259
+ const activeElement = document.activeElement;
13260
+ if (activeElement && confirmModal.contains(activeElement)) {
13261
+ return;
13262
+ }
13263
+ getConfirmModalPreferredButton()?.focus({ preventScroll: true });
13264
+ });
13265
+ };
13266
+
13267
+ confirmModalCancel.addEventListener('focus', () => {
13268
+ confirmModalState.preferredFocus = 'cancel';
13269
+ });
13270
+
13271
+ confirmModalConfirm.addEventListener('focus', () => {
13272
+ confirmModalState.preferredFocus = 'confirm';
13273
+ });
13274
+
13275
+ confirmModalCancel.addEventListener('click', () => {
13276
+ settleConfirmModal(false);
13277
+ });
13278
+
13279
+ confirmModalConfirm.addEventListener('click', () => {
13280
+ settleConfirmModal(true);
13281
+ });
13282
+
13283
+ confirmModal.addEventListener('click', (event) => {
13284
+ if (event.target === confirmModal) {
13285
+ settleConfirmModal(false);
13286
+ }
13287
+ });
13288
+
13289
+ confirmModal.addEventListener('focusout', () => {
13290
+ focusPreferredConfirmButton();
13291
+ });
13292
+
13293
+ confirmModal.addEventListener('keydown', (event) => {
13294
+ if (event.key === 'Escape') {
13295
+ event.preventDefault();
13296
+ settleConfirmModal(false);
13297
+ return;
13298
+ }
13299
+ if (event.key === 'ArrowLeft') {
13300
+ event.preventDefault();
13301
+ event.stopPropagation();
13302
+ moveConfirmModalFocus(-1);
13303
+ return;
13304
+ }
13305
+ if (event.key === 'ArrowRight') {
13306
+ event.preventDefault();
13307
+ event.stopPropagation();
13308
+ moveConfirmModalFocus(1);
13309
+ return;
13310
+ }
13311
+ if (event.key === 'Tab') {
13312
+ event.preventDefault();
13313
+ event.stopPropagation();
13314
+ moveConfirmModalFocus(event.shiftKey ? -1 : 1);
13315
+ return;
13316
+ }
13317
+ if (event.key === 'Enter') {
13318
+ event.preventDefault();
13319
+ event.stopPropagation();
13320
+ if (document.activeElement === confirmModalCancel) {
13321
+ settleConfirmModal(false);
13322
+ return;
13323
+ }
13324
+ settleConfirmModal(true);
13325
+ }
13326
+ });
13327
+ }
13328
+
11842
13329
  if (loginForm && passwordInput) {
11843
13330
  loginForm.addEventListener('submit', async (e) => {
11844
13331
  e.preventDefault();