tabminal 3.0.10 → 3.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +1843 -171
- 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
|
|
@@ -97,6 +103,7 @@ const editorPane = document.getElementById('editor-pane');
|
|
|
97
103
|
// #region Configuration
|
|
98
104
|
const HEARTBEAT_INTERVAL_MS = 1000;
|
|
99
105
|
const RECONNECT_RETRY_MS = 5000;
|
|
106
|
+
const FILE_TREE_REFRESH_INTERVAL_MS = 3000;
|
|
100
107
|
const MAIN_SERVER_ID = 'main';
|
|
101
108
|
const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
|
|
102
109
|
const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
|
|
@@ -104,6 +111,14 @@ const RECENT_AGENT_USAGE_STORAGE_KEY = 'tabminal_recent_agent_usage';
|
|
|
104
111
|
const FILE_WORKSPACE_TAB_PREFIX = 'file:';
|
|
105
112
|
const AGENT_WORKSPACE_TAB_PREFIX = 'agent:';
|
|
106
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
|
+
]);
|
|
107
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>';
|
|
108
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>';
|
|
109
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>';
|
|
@@ -118,6 +133,10 @@ const THOUGHT_SELECT_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15"
|
|
|
118
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>';
|
|
119
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>';
|
|
120
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>';
|
|
121
140
|
const TERMINAL_FONT_FAMILY = '\'Monaspace Neon\', "SF Mono Terminal", '
|
|
122
141
|
+ '"SFMono-Regular", "SF Mono", "JetBrains Mono", Menlo, Consolas, '
|
|
123
142
|
+ 'monospace';
|
|
@@ -170,6 +189,18 @@ function isCompactWorkspaceMode() {
|
|
|
170
189
|
return !!window.__tabminalCompactWorkspaceMode;
|
|
171
190
|
}
|
|
172
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
|
+
|
|
173
204
|
function isCompactTerminalTabsMode() {
|
|
174
205
|
return !!window.__tabminalCompactTerminalTabsMode;
|
|
175
206
|
}
|
|
@@ -668,6 +699,7 @@ class EditorManager {
|
|
|
668
699
|
this.currentSession = null;
|
|
669
700
|
this.iconMap = null;
|
|
670
701
|
this.agentTimestampTimer = null;
|
|
702
|
+
this.treeRefreshTimer = null;
|
|
671
703
|
|
|
672
704
|
// DOM Elements
|
|
673
705
|
this.pane = document.getElementById('editor-pane');
|
|
@@ -1441,90 +1473,1476 @@ class EditorManager {
|
|
|
1441
1473
|
return session ? session.server.modelStore : null;
|
|
1442
1474
|
}
|
|
1443
1475
|
|
|
1444
|
-
getModel(filePath, session = this.currentSession) {
|
|
1445
|
-
const store = this.getModelStore(session);
|
|
1446
|
-
if (!store) return null;
|
|
1447
|
-
return store.get(filePath) || null;
|
|
1448
|
-
}
|
|
1476
|
+
getModel(filePath, session = this.currentSession) {
|
|
1477
|
+
const store = this.getModelStore(session);
|
|
1478
|
+
if (!store) return null;
|
|
1479
|
+
return store.get(filePath) || null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
setModel(filePath, value, session = this.currentSession) {
|
|
1483
|
+
const store = this.getModelStore(session);
|
|
1484
|
+
if (!store) return;
|
|
1485
|
+
store.set(filePath, value);
|
|
1486
|
+
}
|
|
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
|
+
|
|
2072
|
+
async loadIconMap() {
|
|
2073
|
+
try {
|
|
2074
|
+
const res = await fetch('/icons/map.json');
|
|
2075
|
+
this.iconMap = await res.json();
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
console.error('Failed to load icon map', e);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
getIcon(name, isDirectory, isExpanded) {
|
|
2082
|
+
if (!this.iconMap) return isDirectory ? (isExpanded ? '📂' : '📁') : '📄';
|
|
2083
|
+
|
|
2084
|
+
if (isDirectory) {
|
|
2085
|
+
const folderIcon = isExpanded ? (this.iconMap.folderOpen || 'folder-src-open') : (this.iconMap.folder || 'folder-src');
|
|
2086
|
+
return `<img src="/icons/${folderIcon}.svg" class="file-icon" alt="folder">`;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const lowerName = name.toLowerCase();
|
|
2090
|
+
if (this.iconMap.filenames[lowerName]) {
|
|
2091
|
+
return `<img src="/icons/${this.iconMap.filenames[lowerName]}.svg" class="file-icon" alt="file">`;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
const parts = name.split('.');
|
|
2095
|
+
if (parts.length > 1) {
|
|
2096
|
+
const ext = parts.pop().toLowerCase();
|
|
2097
|
+
if (this.iconMap.extensions[ext]) {
|
|
2098
|
+
return `<img src="/icons/${this.iconMap.extensions[ext]}.svg" class="file-icon" alt="file">`;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
return `<img src="/icons/${this.iconMap.default || 'document'}.svg" class="file-icon" alt="file">`;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
initResizer() {
|
|
2106
|
+
let startY, startHeight;
|
|
2107
|
+
const onMouseMove = (e) => {
|
|
2108
|
+
const dy = e.clientY - startY;
|
|
2109
|
+
const newHeight = startHeight + dy;
|
|
2110
|
+
const containerHeight = this.pane.parentElement.clientHeight;
|
|
2111
|
+
const resizerHeight = this.resizer.offsetHeight;
|
|
2112
|
+
|
|
2113
|
+
if (newHeight > 100 && newHeight < containerHeight - resizerHeight - 50) {
|
|
2114
|
+
const flex = `0 0 ${newHeight}px`;
|
|
2115
|
+
this.pane.style.flex = flex;
|
|
2116
|
+
if (this.currentSession) {
|
|
2117
|
+
this.currentSession.layoutState.editorFlex = flex;
|
|
2118
|
+
}
|
|
2119
|
+
this.layout();
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
const onMouseUp = () => {
|
|
2123
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
2124
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
2125
|
+
document.body.style.cursor = '';
|
|
2126
|
+
const termWrapper = document.getElementById('terminal-wrapper');
|
|
2127
|
+
if (termWrapper) termWrapper.style.pointerEvents = '';
|
|
2128
|
+
};
|
|
2129
|
+
this.resizer.addEventListener('mousedown', (e) => {
|
|
2130
|
+
startY = e.clientY;
|
|
2131
|
+
startHeight = this.pane.offsetHeight;
|
|
2132
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
2133
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
2134
|
+
document.body.style.cursor = 'row-resize';
|
|
2135
|
+
const termWrapper = document.getElementById('terminal-wrapper');
|
|
2136
|
+
if (termWrapper) termWrapper.style.pointerEvents = 'none';
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
refreshSessionTree(session) {
|
|
2141
|
+
if (!session || !session.fileTreeElement) return;
|
|
2142
|
+
session.fileTreeRenderToken = (session.fileTreeRenderToken || 0) + 1;
|
|
2143
|
+
const renderToken = session.fileTreeRenderToken;
|
|
2144
|
+
const scrollTop = session.fileTreeElement.scrollTop;
|
|
2145
|
+
void this.renderTree(
|
|
2146
|
+
session.cwd,
|
|
2147
|
+
session.fileTreeElement,
|
|
2148
|
+
session,
|
|
2149
|
+
renderToken
|
|
2150
|
+
).finally(() => {
|
|
2151
|
+
if (
|
|
2152
|
+
session.fileTreeElement
|
|
2153
|
+
&& session.fileTreeRenderToken === renderToken
|
|
2154
|
+
) {
|
|
2155
|
+
session.fileTreeElement.scrollTop = scrollTop;
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
this.updateTreeAutoRefresh();
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
isSessionTreeVisible(session) {
|
|
2162
|
+
return !!session?.fileTreeElement && !!session?.editorState?.isVisible;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
canRefreshSessionTree(session) {
|
|
2166
|
+
return this.isSessionTreeVisible(session) && !session.treeEditingPath;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
refreshVisibleSessionTrees() {
|
|
2170
|
+
for (const session of state.sessions.values()) {
|
|
2171
|
+
if (this.canRefreshSessionTree(session)) {
|
|
2172
|
+
this.requestSessionTreeRefresh(session);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
requestSessionTreeRefresh(session, { force = false } = {}) {
|
|
2178
|
+
if (!force && !this.canRefreshSessionTree(session)) {
|
|
2179
|
+
this.updateTreeAutoRefresh();
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
if (session.fileTreeRefreshQueued) return;
|
|
2183
|
+
session.fileTreeRefreshQueued = true;
|
|
2184
|
+
requestAnimationFrame(() => {
|
|
2185
|
+
session.fileTreeRefreshQueued = false;
|
|
2186
|
+
if (force || this.canRefreshSessionTree(session)) {
|
|
2187
|
+
this.refreshSessionTree(session);
|
|
2188
|
+
} else {
|
|
2189
|
+
this.updateTreeAutoRefresh();
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
updateTreeAutoRefresh() {
|
|
2195
|
+
const shouldRun = (
|
|
2196
|
+
document.visibilityState === 'visible'
|
|
2197
|
+
&& Array.from(state.sessions.values()).some(
|
|
2198
|
+
(session) => this.canRefreshSessionTree(session)
|
|
2199
|
+
)
|
|
2200
|
+
);
|
|
2201
|
+
if (shouldRun && !this.treeRefreshTimer) {
|
|
2202
|
+
this.treeRefreshTimer = window.setInterval(() => {
|
|
2203
|
+
if (document.visibilityState !== 'visible') {
|
|
2204
|
+
this.updateTreeAutoRefresh();
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const hasVisibleTrees = Array.from(
|
|
2208
|
+
state.sessions.values()
|
|
2209
|
+
).some((session) => this.canRefreshSessionTree(session));
|
|
2210
|
+
if (!hasVisibleTrees) {
|
|
2211
|
+
this.updateTreeAutoRefresh();
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
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
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
ensureTreeList(container) {
|
|
2570
|
+
const existing = Array.from(container.children).find(
|
|
2571
|
+
(child) => child.tagName === 'UL'
|
|
2572
|
+
);
|
|
2573
|
+
if (existing) return existing;
|
|
2574
|
+
const list = document.createElement('ul');
|
|
2575
|
+
container.appendChild(list);
|
|
2576
|
+
return list;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
getTreeChildList(item) {
|
|
2580
|
+
return Array.from(item.children).find((child) => child.tagName === 'UL')
|
|
2581
|
+
|| null;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
getTreeItemExpanded(filePath, session) {
|
|
2585
|
+
return session.sharedWorkspaceState.expandedPaths.includes(filePath);
|
|
2586
|
+
}
|
|
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
|
+
|
|
2655
|
+
updateTreeItem(li, file, session, renderToken) {
|
|
2656
|
+
li.dataset.path = file.path;
|
|
2657
|
+
li.dataset.isDirectory = file.isDirectory ? '1' : '0';
|
|
2658
|
+
li.dataset.renameable = file.renameable ? '1' : '0';
|
|
2659
|
+
li.dataset.deleteable = file.deleteable ? '1' : '0';
|
|
2660
|
+
|
|
2661
|
+
let row = Array.from(li.children).find(
|
|
2662
|
+
(child) => child.classList?.contains('file-tree-item')
|
|
2663
|
+
);
|
|
2664
|
+
if (!row) {
|
|
2665
|
+
row = document.createElement('div');
|
|
2666
|
+
row.className = 'file-tree-item';
|
|
2667
|
+
li.prepend(row);
|
|
2668
|
+
}
|
|
2669
|
+
row.tabIndex = -1;
|
|
2670
|
+
|
|
2671
|
+
let icon = row.querySelector('.icon');
|
|
2672
|
+
if (!icon) {
|
|
2673
|
+
icon = document.createElement('span');
|
|
2674
|
+
icon.className = 'icon';
|
|
2675
|
+
row.appendChild(icon);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
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');
|
|
2701
|
+
if (!name) {
|
|
2702
|
+
name = document.createElement('span');
|
|
2703
|
+
name.className = 'file-tree-name';
|
|
2704
|
+
row.appendChild(name);
|
|
2705
|
+
}
|
|
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
|
+
|
|
2719
|
+
row.className = 'file-tree-item';
|
|
2720
|
+
if (file.isDirectory) {
|
|
2721
|
+
row.classList.add('is-dir');
|
|
2722
|
+
}
|
|
2723
|
+
row.classList.toggle(
|
|
2724
|
+
'active',
|
|
2725
|
+
!file.isDirectory
|
|
2726
|
+
&& session.editorState.activeFilePath === file.path
|
|
2727
|
+
);
|
|
2728
|
+
row.classList.toggle(
|
|
2729
|
+
'selected',
|
|
2730
|
+
session.selectedTreePath === file.path
|
|
2731
|
+
);
|
|
2732
|
+
row.classList.toggle('editing', isEditing);
|
|
2733
|
+
|
|
2734
|
+
const isExpanded = file.isDirectory
|
|
2735
|
+
&& this.getTreeItemExpanded(file.path, session);
|
|
2736
|
+
li.classList.toggle('expanded', isExpanded);
|
|
2737
|
+
icon.innerHTML = this.getIcon(file.name, file.isDirectory, isExpanded);
|
|
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
|
+
}
|
|
2816
|
+
|
|
2817
|
+
row.onclick = async (e) => {
|
|
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 });
|
|
2832
|
+
if (file.isDirectory) {
|
|
2833
|
+
if (li.classList.contains('expanded')) {
|
|
2834
|
+
li.classList.remove('expanded');
|
|
2835
|
+
session.sharedWorkspaceState.expandedPaths =
|
|
2836
|
+
session.sharedWorkspaceState.expandedPaths
|
|
2837
|
+
.filter((path) => path !== file.path);
|
|
2838
|
+
session.saveState({ touchWorkspace: true });
|
|
2839
|
+
void session.server.fetch('/api/memory/expand', {
|
|
2840
|
+
method: 'POST',
|
|
2841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2842
|
+
body: JSON.stringify({
|
|
2843
|
+
path: file.path,
|
|
2844
|
+
expanded: false
|
|
2845
|
+
})
|
|
2846
|
+
});
|
|
2847
|
+
icon.innerHTML = this.getIcon(file.name, true, false);
|
|
2848
|
+
const childUl = this.getTreeChildList(li);
|
|
2849
|
+
if (childUl) {
|
|
2850
|
+
childUl.remove();
|
|
2851
|
+
}
|
|
2852
|
+
this.updateTreeAutoRefresh();
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
li.classList.add('expanded');
|
|
2857
|
+
session.sharedWorkspaceState.expandedPaths =
|
|
2858
|
+
uniqueStringList([
|
|
2859
|
+
...session.sharedWorkspaceState.expandedPaths,
|
|
2860
|
+
file.path
|
|
2861
|
+
]);
|
|
2862
|
+
session.saveState({ touchWorkspace: true });
|
|
2863
|
+
void session.server.fetch('/api/memory/expand', {
|
|
2864
|
+
method: 'POST',
|
|
2865
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2866
|
+
body: JSON.stringify({
|
|
2867
|
+
path: file.path,
|
|
2868
|
+
expanded: true
|
|
2869
|
+
})
|
|
2870
|
+
});
|
|
1449
2871
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2872
|
+
icon.innerHTML = this.getIcon(file.name, true, true);
|
|
2873
|
+
await this.renderTree(file.path, li, session, renderToken);
|
|
2874
|
+
this.updateTreeAutoRefresh();
|
|
2875
|
+
session.fileTreeElement?.focus({ preventScroll: true });
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
1455
2878
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
this.
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
2879
|
+
await this.openFile(file.path, session, {
|
|
2880
|
+
focusEditor: false
|
|
2881
|
+
});
|
|
2882
|
+
this.focusTreePath(session, file.path);
|
|
2883
|
+
session.pendingTreeFocusPath = file.path;
|
|
2884
|
+
this.requestSessionTreeRefresh(session);
|
|
2885
|
+
};
|
|
1464
2886
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
+
};
|
|
1472
2898
|
|
|
1473
|
-
|
|
1474
|
-
if (this.iconMap.filenames[lowerName]) {
|
|
1475
|
-
return `<img src="/icons/${this.iconMap.filenames[lowerName]}.svg" class="file-icon" alt="file">`;
|
|
1476
|
-
}
|
|
2899
|
+
row.onkeydown = null;
|
|
1477
2900
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
return `<img src="/icons/${this.iconMap.extensions[ext]}.svg" class="file-icon" alt="file">`;
|
|
2901
|
+
if (!isExpanded) {
|
|
2902
|
+
const childUl = this.getTreeChildList(li);
|
|
2903
|
+
if (childUl) {
|
|
2904
|
+
childUl.remove();
|
|
1483
2905
|
}
|
|
1484
2906
|
}
|
|
1485
2907
|
|
|
1486
|
-
|
|
2908
|
+
if (session.pendingTreeFocusPath === file.path) {
|
|
2909
|
+
session.pendingTreeFocusPath = '';
|
|
2910
|
+
requestAnimationFrame(() => {
|
|
2911
|
+
row.scrollIntoView({ block: 'nearest' });
|
|
2912
|
+
session.fileTreeElement?.focus({ preventScroll: true });
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
1487
2915
|
}
|
|
1488
2916
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const containerHeight = this.pane.parentElement.clientHeight;
|
|
1495
|
-
const resizerHeight = this.resizer.offsetHeight;
|
|
1496
|
-
|
|
1497
|
-
if (newHeight > 100 && newHeight < containerHeight - resizerHeight - 50) {
|
|
1498
|
-
const flex = `0 0 ${newHeight}px`;
|
|
1499
|
-
this.pane.style.flex = flex;
|
|
1500
|
-
if (this.currentSession) {
|
|
1501
|
-
this.currentSession.layoutState.editorFlex = flex;
|
|
1502
|
-
}
|
|
1503
|
-
this.layout();
|
|
2917
|
+
reconcileTreeList(list, dirPath, files, creatable, session, renderToken) {
|
|
2918
|
+
const existingItems = new Map();
|
|
2919
|
+
Array.from(list.children).forEach((child) => {
|
|
2920
|
+
if (child.tagName === 'LI' && child.dataset.path) {
|
|
2921
|
+
existingItems.set(child.dataset.path, child);
|
|
1504
2922
|
}
|
|
1505
|
-
};
|
|
1506
|
-
const onMouseUp = () => {
|
|
1507
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
1508
|
-
document.removeEventListener('mouseup', onMouseUp);
|
|
1509
|
-
document.body.style.cursor = '';
|
|
1510
|
-
const termWrapper = document.getElementById('terminal-wrapper');
|
|
1511
|
-
if (termWrapper) termWrapper.style.pointerEvents = '';
|
|
1512
|
-
};
|
|
1513
|
-
this.resizer.addEventListener('mousedown', (e) => {
|
|
1514
|
-
startY = e.clientY;
|
|
1515
|
-
startHeight = this.pane.offsetHeight;
|
|
1516
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
1517
|
-
document.addEventListener('mouseup', onMouseUp);
|
|
1518
|
-
document.body.style.cursor = 'row-resize';
|
|
1519
|
-
const termWrapper = document.getElementById('terminal-wrapper');
|
|
1520
|
-
if (termWrapper) termWrapper.style.pointerEvents = 'none';
|
|
1521
2923
|
});
|
|
1522
|
-
}
|
|
1523
2924
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2925
|
+
const orderedItems = [];
|
|
2926
|
+
for (const file of files) {
|
|
2927
|
+
let li = existingItems.get(file.path) || null;
|
|
2928
|
+
if (!li) {
|
|
2929
|
+
li = document.createElement('li');
|
|
2930
|
+
} else {
|
|
2931
|
+
existingItems.delete(file.path);
|
|
2932
|
+
}
|
|
2933
|
+
this.updateTreeItem(li, file, session, renderToken);
|
|
2934
|
+
orderedItems.push(li);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
for (const li of existingItems.values()) {
|
|
2938
|
+
li.remove();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
for (const li of orderedItems) {
|
|
2942
|
+
list.appendChild(li);
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
this.updateTreeCreateRow(list, dirPath, creatable, session);
|
|
1528
2946
|
}
|
|
1529
2947
|
|
|
1530
2948
|
initMonaco() {
|
|
@@ -1656,6 +3074,7 @@ class EditorManager {
|
|
|
1656
3074
|
this.updateEditorPaneVisibility();
|
|
1657
3075
|
}
|
|
1658
3076
|
|
|
3077
|
+
this.updateTreeAutoRefresh();
|
|
1659
3078
|
session.updateTabUI();
|
|
1660
3079
|
session.saveState({ touchWorkspace: true });
|
|
1661
3080
|
}
|
|
@@ -1694,6 +3113,7 @@ class EditorManager {
|
|
|
1694
3113
|
|
|
1695
3114
|
this.updateEditorPaneVisibility();
|
|
1696
3115
|
this.updateTerminalLayoutButton();
|
|
3116
|
+
this.updateTreeAutoRefresh();
|
|
1697
3117
|
|
|
1698
3118
|
// Restore layout
|
|
1699
3119
|
if (session.layoutState) {
|
|
@@ -1719,102 +3139,62 @@ class EditorManager {
|
|
|
1719
3139
|
}
|
|
1720
3140
|
}
|
|
1721
3141
|
|
|
1722
|
-
async renderTree(
|
|
3142
|
+
async renderTree(
|
|
3143
|
+
dirPath,
|
|
3144
|
+
container,
|
|
3145
|
+
session,
|
|
3146
|
+
renderToken = session?.fileTreeRenderToken || 0
|
|
3147
|
+
) {
|
|
1723
3148
|
try {
|
|
1724
|
-
const res = await session.server.fetch(
|
|
3149
|
+
const res = await session.server.fetch(
|
|
3150
|
+
`/api/fs/list?path=${encodeURIComponent(dirPath)}`
|
|
3151
|
+
);
|
|
1725
3152
|
if (!res.ok) return;
|
|
1726
|
-
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;
|
|
3162
|
+
if ((session.fileTreeRenderToken || 0) !== renderToken) return;
|
|
3163
|
+
|
|
3164
|
+
const list = this.ensureTreeList(container);
|
|
3165
|
+
this.reconcileTreeList(
|
|
3166
|
+
list,
|
|
3167
|
+
dirPath,
|
|
3168
|
+
files,
|
|
3169
|
+
creatable,
|
|
3170
|
+
session,
|
|
3171
|
+
renderToken
|
|
3172
|
+
);
|
|
3173
|
+
if ((session.fileTreeRenderToken || 0) !== renderToken) return;
|
|
1727
3174
|
|
|
1728
|
-
const ul = document.createElement('ul');
|
|
1729
|
-
|
|
1730
3175
|
for (const file of files) {
|
|
1731
|
-
const li = document.createElement('li');
|
|
1732
|
-
const div = document.createElement('div');
|
|
1733
|
-
div.className = 'file-tree-item';
|
|
1734
|
-
if (file.isDirectory) div.classList.add('is-dir');
|
|
1735
|
-
|
|
1736
|
-
let isExpanded = false;
|
|
1737
3176
|
if (
|
|
1738
3177
|
file.isDirectory
|
|
1739
|
-
&&
|
|
1740
|
-
file.path
|
|
1741
|
-
)
|
|
3178
|
+
&& this.getTreeItemExpanded(file.path, session)
|
|
1742
3179
|
) {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
icon.className = 'icon';
|
|
1749
|
-
icon.innerHTML = this.getIcon(file.name, file.isDirectory, isExpanded);
|
|
1750
|
-
|
|
1751
|
-
const name = document.createElement('span');
|
|
1752
|
-
name.textContent = file.name;
|
|
1753
|
-
|
|
1754
|
-
div.appendChild(icon);
|
|
1755
|
-
div.appendChild(name);
|
|
1756
|
-
|
|
1757
|
-
div.addEventListener('click', async (e) => {
|
|
1758
|
-
e.stopPropagation();
|
|
1759
|
-
if (file.isDirectory) {
|
|
1760
|
-
if (li.classList.contains('expanded')) {
|
|
1761
|
-
li.classList.remove('expanded');
|
|
1762
|
-
session.sharedWorkspaceState.expandedPaths =
|
|
1763
|
-
session.sharedWorkspaceState.expandedPaths
|
|
1764
|
-
.filter((path) => path !== file.path);
|
|
1765
|
-
session.saveState({ touchWorkspace: true });
|
|
1766
|
-
void session.server.fetch('/api/memory/expand', {
|
|
1767
|
-
method: 'POST',
|
|
1768
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1769
|
-
body: JSON.stringify({
|
|
1770
|
-
path: file.path,
|
|
1771
|
-
expanded: false
|
|
1772
|
-
})
|
|
1773
|
-
});
|
|
1774
|
-
|
|
1775
|
-
icon.innerHTML = this.getIcon(file.name, true, false);
|
|
1776
|
-
const childUl = li.querySelector('ul');
|
|
1777
|
-
if (childUl) childUl.remove();
|
|
1778
|
-
} else {
|
|
1779
|
-
li.classList.add('expanded');
|
|
1780
|
-
session.sharedWorkspaceState.expandedPaths =
|
|
1781
|
-
uniqueStringList([
|
|
1782
|
-
...session.sharedWorkspaceState.expandedPaths,
|
|
1783
|
-
file.path
|
|
1784
|
-
]);
|
|
1785
|
-
session.saveState({ touchWorkspace: true });
|
|
1786
|
-
void session.server.fetch('/api/memory/expand', {
|
|
1787
|
-
method: 'POST',
|
|
1788
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1789
|
-
body: JSON.stringify({
|
|
1790
|
-
path: file.path,
|
|
1791
|
-
expanded: true
|
|
1792
|
-
})
|
|
1793
|
-
});
|
|
1794
|
-
|
|
1795
|
-
icon.innerHTML = this.getIcon(file.name, true, true);
|
|
1796
|
-
await this.renderTree(file.path, li, session);
|
|
1797
|
-
}
|
|
1798
|
-
} else {
|
|
1799
|
-
await this.openFile(file.path, session);
|
|
3180
|
+
const item = Array.from(list.children).find(
|
|
3181
|
+
(child) => child.dataset.path === file.path
|
|
3182
|
+
);
|
|
3183
|
+
if (item) {
|
|
3184
|
+
void this.renderTree(file.path, item, session, renderToken);
|
|
1800
3185
|
}
|
|
1801
|
-
});
|
|
1802
|
-
|
|
1803
|
-
li.appendChild(div);
|
|
1804
|
-
|
|
1805
|
-
if (isExpanded) {
|
|
1806
|
-
this.renderTree(file.path, li, session);
|
|
1807
3186
|
}
|
|
1808
|
-
|
|
1809
|
-
ul.appendChild(li);
|
|
1810
3187
|
}
|
|
1811
|
-
container.appendChild(ul);
|
|
1812
3188
|
} catch (err) {
|
|
1813
3189
|
console.error('Failed to render tree:', err);
|
|
1814
3190
|
}
|
|
1815
3191
|
}
|
|
1816
3192
|
|
|
1817
|
-
async openFile(
|
|
3193
|
+
async openFile(
|
|
3194
|
+
filePath,
|
|
3195
|
+
sessionOrRestore = this.currentSession,
|
|
3196
|
+
options = {}
|
|
3197
|
+
) {
|
|
1818
3198
|
const session = typeof sessionOrRestore === 'boolean'
|
|
1819
3199
|
? this.currentSession
|
|
1820
3200
|
: sessionOrRestore;
|
|
@@ -1827,20 +3207,10 @@ class EditorManager {
|
|
|
1827
3207
|
: session;
|
|
1828
3208
|
if (!targetSession) return;
|
|
1829
3209
|
const state = targetSession.editorState;
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (!state.openFiles.includes(filePath)) {
|
|
1833
|
-
state.openFiles.push(filePath);
|
|
1834
|
-
this.renderEditorTabs();
|
|
1835
|
-
touchedWorkspace = true;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
this.updateEditorPaneVisibility();
|
|
3210
|
+
const wasOpen = state.openFiles.includes(filePath);
|
|
3211
|
+
const isImage = isSupportedImagePath(filePath);
|
|
1839
3212
|
|
|
1840
3213
|
if (!this.getModel(filePath)) {
|
|
1841
|
-
const ext = filePath.split('.').pop().toLowerCase();
|
|
1842
|
-
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext);
|
|
1843
|
-
|
|
1844
3214
|
let model = null;
|
|
1845
3215
|
let content = null;
|
|
1846
3216
|
let readonly = false;
|
|
@@ -1850,7 +3220,20 @@ class EditorManager {
|
|
|
1850
3220
|
const res = await targetSession.server.fetch(
|
|
1851
3221
|
`/api/fs/read?path=${encodeURIComponent(filePath)}`
|
|
1852
3222
|
);
|
|
1853
|
-
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
|
+
}
|
|
1854
3237
|
const data = await res.json();
|
|
1855
3238
|
content = data.content;
|
|
1856
3239
|
readonly = data.readonly;
|
|
@@ -1880,7 +3263,16 @@ class EditorManager {
|
|
|
1880
3263
|
});
|
|
1881
3264
|
}
|
|
1882
3265
|
|
|
1883
|
-
|
|
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);
|
|
1884
3276
|
if (touchedWorkspace) {
|
|
1885
3277
|
targetSession.saveState({ touchWorkspace: true });
|
|
1886
3278
|
}
|
|
@@ -2087,9 +3479,10 @@ class EditorManager {
|
|
|
2087
3479
|
});
|
|
2088
3480
|
}
|
|
2089
3481
|
|
|
2090
|
-
activateFileTab(filePath, isRestore = false) {
|
|
3482
|
+
activateFileTab(filePath, isRestore = false, options = {}) {
|
|
2091
3483
|
if (!this.currentSession) return;
|
|
2092
3484
|
if (!filePath) return;
|
|
3485
|
+
const focusEditor = options.focusEditor !== false;
|
|
2093
3486
|
const state = this.currentSession.editorState;
|
|
2094
3487
|
|
|
2095
3488
|
if (!isRestore && state.activeFilePath && state.activeFilePath !== filePath) {
|
|
@@ -2111,7 +3504,7 @@ class EditorManager {
|
|
|
2111
3504
|
this.syncTerminalWorkspacePlacement();
|
|
2112
3505
|
|
|
2113
3506
|
if (!file) {
|
|
2114
|
-
this.openFile(filePath, true);
|
|
3507
|
+
this.openFile(filePath, true, options);
|
|
2115
3508
|
return;
|
|
2116
3509
|
}
|
|
2117
3510
|
|
|
@@ -2146,7 +3539,9 @@ class EditorManager {
|
|
|
2146
3539
|
if (savedViewState) {
|
|
2147
3540
|
this.editor.restoreViewState(savedViewState);
|
|
2148
3541
|
}
|
|
2149
|
-
|
|
3542
|
+
if (focusEditor) {
|
|
3543
|
+
this.editor.focus();
|
|
3544
|
+
}
|
|
2150
3545
|
// Force layout to ensure content is visible
|
|
2151
3546
|
requestAnimationFrame(() => this.editor.layout());
|
|
2152
3547
|
}
|
|
@@ -4640,6 +6035,11 @@ class Session {
|
|
|
4640
6035
|
this.layoutState = {
|
|
4641
6036
|
editorFlex: '2 1 0%'
|
|
4642
6037
|
};
|
|
6038
|
+
this.selectedTreePath = '';
|
|
6039
|
+
this.treeEditingPath = '';
|
|
6040
|
+
this.treeRenameSubmitting = false;
|
|
6041
|
+
this.pendingTreeFocusPath = '';
|
|
6042
|
+
this.pendingTreeRenameFocusPath = '';
|
|
4643
6043
|
this.previewRelayoutScheduled = false;
|
|
4644
6044
|
this.lastTerminalControlClaimAt = 0;
|
|
4645
6045
|
this.boundTerminalClaimRoot = null;
|
|
@@ -4884,11 +6284,12 @@ class Session {
|
|
|
4884
6284
|
if (workspaceChanged) {
|
|
4885
6285
|
if (this.fileTreeElement) {
|
|
4886
6286
|
if (this.editorState.isVisible) {
|
|
4887
|
-
editorManager.
|
|
6287
|
+
editorManager.requestSessionTreeRefresh(this);
|
|
4888
6288
|
} else {
|
|
4889
6289
|
this.fileTreeElement.innerHTML = '';
|
|
4890
6290
|
}
|
|
4891
6291
|
}
|
|
6292
|
+
editorManager.updateTreeAutoRefresh();
|
|
4892
6293
|
}
|
|
4893
6294
|
if (workspaceChanged && state.activeSessionKey === this.key) {
|
|
4894
6295
|
refreshWorkspaceIfSessionActive(this);
|
|
@@ -5146,6 +6547,9 @@ class Session {
|
|
|
5146
6547
|
this.runningCommand = '';
|
|
5147
6548
|
this.needsAttention = false;
|
|
5148
6549
|
this.updateTabUI();
|
|
6550
|
+
if (this.editorState.isVisible) {
|
|
6551
|
+
editorManager.requestSessionTreeRefresh(this);
|
|
6552
|
+
}
|
|
5149
6553
|
if (state.activeSessionKey === this.key) {
|
|
5150
6554
|
editorManager.renderEditorTabs();
|
|
5151
6555
|
}
|
|
@@ -5205,6 +6609,10 @@ class Session {
|
|
|
5205
6609
|
this.needsAttention = false;
|
|
5206
6610
|
}
|
|
5207
6611
|
|
|
6612
|
+
if (this.editorState.isVisible) {
|
|
6613
|
+
editorManager.requestSessionTreeRefresh(this);
|
|
6614
|
+
}
|
|
6615
|
+
|
|
5208
6616
|
this.updateTabUI();
|
|
5209
6617
|
if (state.activeSessionKey === this.key) {
|
|
5210
6618
|
editorManager.renderEditorTabs();
|
|
@@ -10873,10 +12281,69 @@ function createTabElement(session) {
|
|
|
10873
12281
|
|
|
10874
12282
|
const fileTree = document.createElement('div');
|
|
10875
12283
|
fileTree.className = 'tab-file-tree';
|
|
12284
|
+
fileTree.tabIndex = 0;
|
|
10876
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
|
+
});
|
|
10877
12344
|
|
|
10878
12345
|
if (session.editorState && session.editorState.isVisible) {
|
|
10879
|
-
editorManager.
|
|
12346
|
+
editorManager.refreshSessionTree(session);
|
|
10880
12347
|
}
|
|
10881
12348
|
tab.appendChild(fileTree);
|
|
10882
12349
|
|
|
@@ -11090,6 +12557,127 @@ function openServerModal(mode, server = null) {
|
|
|
11090
12557
|
return true;
|
|
11091
12558
|
}
|
|
11092
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
|
+
|
|
11093
12681
|
function renderServerControls() {
|
|
11094
12682
|
if (!serverControlsEl) return;
|
|
11095
12683
|
serverControlsEl.innerHTML = '';
|
|
@@ -11244,10 +12832,14 @@ document.addEventListener('keydown', noteAppInteraction, {
|
|
|
11244
12832
|
window.addEventListener('focus', () => {
|
|
11245
12833
|
noteAppInteraction();
|
|
11246
12834
|
enterAppNotificationQuietPeriod();
|
|
12835
|
+
editorManager.refreshVisibleSessionTrees();
|
|
12836
|
+
editorManager.updateTreeAutoRefresh();
|
|
11247
12837
|
});
|
|
11248
12838
|
window.addEventListener('pageshow', () => {
|
|
11249
12839
|
noteAppInteraction();
|
|
11250
12840
|
enterAppNotificationQuietPeriod();
|
|
12841
|
+
editorManager.refreshVisibleSessionTrees();
|
|
12842
|
+
editorManager.updateTreeAutoRefresh();
|
|
11251
12843
|
});
|
|
11252
12844
|
|
|
11253
12845
|
document.addEventListener('click', () => {
|
|
@@ -11258,7 +12850,9 @@ document.addEventListener('visibilitychange', () => {
|
|
|
11258
12850
|
noteAppInteraction();
|
|
11259
12851
|
enterAppNotificationQuietPeriod();
|
|
11260
12852
|
clearVisibleAttentionState();
|
|
12853
|
+
editorManager.refreshVisibleSessionTrees();
|
|
11261
12854
|
}
|
|
12855
|
+
editorManager.updateTreeAutoRefresh();
|
|
11262
12856
|
});
|
|
11263
12857
|
// #endregion
|
|
11264
12858
|
|
|
@@ -11654,6 +13248,84 @@ if (
|
|
|
11654
13248
|
});
|
|
11655
13249
|
}
|
|
11656
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
|
+
|
|
11657
13329
|
if (loginForm && passwordInput) {
|
|
11658
13330
|
loginForm.addEventListener('submit', async (e) => {
|
|
11659
13331
|
e.preventDefault();
|