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/README.md +4 -0
- package/package.json +1 -1
- package/public/app.js +1527 -40
- package/public/index.html +23 -0
- package/public/styles.css +120 -5
- package/src/fs-routes.mjs +294 -24
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.
|
|
2171
|
+
if (this.canRefreshSessionTree(session)) {
|
|
1554
2172
|
this.requestSessionTreeRefresh(session);
|
|
1555
2173
|
}
|
|
1556
2174
|
}
|
|
1557
2175
|
}
|
|
1558
2176
|
|
|
1559
|
-
requestSessionTreeRefresh(session) {
|
|
1560
|
-
if (!this.
|
|
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.
|
|
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.
|
|
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(
|
|
1590
|
-
|
|
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
|
|
1646
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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();
|