@chrysb/alphaclaw 0.9.15 → 0.9.16
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.
|
@@ -76,6 +76,16 @@ const collectFolderPaths = (node, folderPaths) => {
|
|
|
76
76
|
);
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
const collectTruncatedExpandedFolderPaths = (node, expandedPaths, folderPaths) => {
|
|
80
|
+
if (!node || node.type !== "folder") return;
|
|
81
|
+
if (node.truncated && expandedPaths.has(node.path || "")) {
|
|
82
|
+
folderPaths.push(node.path || "");
|
|
83
|
+
}
|
|
84
|
+
(node.children || []).forEach((childNode) =>
|
|
85
|
+
collectTruncatedExpandedFolderPaths(childNode, expandedPaths, folderPaths),
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
79
89
|
const collectFilePaths = (node, filePaths) => {
|
|
80
90
|
if (!node) return;
|
|
81
91
|
if (node.type === "file") {
|
|
@@ -104,6 +114,19 @@ const removeTreePath = (node, targetPath) => {
|
|
|
104
114
|
};
|
|
105
115
|
};
|
|
106
116
|
|
|
117
|
+
const replaceTreeNode = (node, nextNode) => {
|
|
118
|
+
if (!node || !nextNode) return node;
|
|
119
|
+
if (String(node.path || "") === String(nextNode.path || "")) return nextNode;
|
|
120
|
+
if (node.type !== "folder") return node;
|
|
121
|
+
const nextChildren = (node.children || []).map((childNode) =>
|
|
122
|
+
replaceTreeNode(childNode, nextNode),
|
|
123
|
+
);
|
|
124
|
+
return {
|
|
125
|
+
...node,
|
|
126
|
+
children: nextChildren,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
107
130
|
const filterTreeNode = (node, normalizedQuery) => {
|
|
108
131
|
if (!node) return null;
|
|
109
132
|
const query = String(normalizedQuery || "")
|
|
@@ -410,6 +433,7 @@ const TreeNode = ({
|
|
|
410
433
|
onCreationConfirm,
|
|
411
434
|
onCreationCancel,
|
|
412
435
|
dragSourcePath = "",
|
|
436
|
+
loadingFolderPaths = new Set(),
|
|
413
437
|
}) => {
|
|
414
438
|
if (!node) return null;
|
|
415
439
|
if (node.type === "file") {
|
|
@@ -471,6 +495,7 @@ const TreeNode = ({
|
|
|
471
495
|
|
|
472
496
|
const folderPath = node.path || "";
|
|
473
497
|
const isCollapsed = isSearchActive ? false : !expandedPaths.has(folderPath);
|
|
498
|
+
const isLoadingFolder = loadingFolderPaths.has(folderPath);
|
|
474
499
|
const isFolderActive = selectedPath === folderPath;
|
|
475
500
|
const isFolderLocked = folderPath && matchesBrowsePolicyPath(
|
|
476
501
|
kLockedBrowsePaths,
|
|
@@ -484,7 +509,7 @@ const TreeNode = ({
|
|
|
484
509
|
class=${`tree-folder ${isCollapsed ? "collapsed" : ""} ${isFolderActive ? "active" : ""} ${isDropTarget ? "is-drop-target" : ""}`.trim()}
|
|
485
510
|
onclick=${() => {
|
|
486
511
|
if (!folderPath) return;
|
|
487
|
-
onSetFolderExpanded(folderPath, isCollapsed);
|
|
512
|
+
onSetFolderExpanded(folderPath, isCollapsed, node);
|
|
488
513
|
onSelectFolder(folderPath);
|
|
489
514
|
}}
|
|
490
515
|
oncontextmenu=${(e) => {
|
|
@@ -540,8 +565,9 @@ const TreeNode = ({
|
|
|
540
565
|
event.preventDefault();
|
|
541
566
|
event.stopPropagation();
|
|
542
567
|
if (!folderPath) return;
|
|
543
|
-
onSetFolderExpanded(folderPath, isCollapsed);
|
|
568
|
+
onSetFolderExpanded(folderPath, isCollapsed, node);
|
|
544
569
|
}}
|
|
570
|
+
disabled=${isLoadingFolder}
|
|
545
571
|
>
|
|
546
572
|
<span class="arrow">▼</span>
|
|
547
573
|
</button>
|
|
@@ -587,6 +613,7 @@ const TreeNode = ({
|
|
|
587
613
|
onCreationConfirm=${onCreationConfirm}
|
|
588
614
|
onCreationCancel=${onCreationCancel}
|
|
589
615
|
dragSourcePath=${dragSourcePath}
|
|
616
|
+
loadingFolderPaths=${loadingFolderPaths}
|
|
590
617
|
/>
|
|
591
618
|
`,
|
|
592
619
|
)}
|
|
@@ -623,6 +650,7 @@ const TreeNode = ({
|
|
|
623
650
|
onCreationConfirm=${onCreationConfirm}
|
|
624
651
|
onCreationCancel=${onCreationCancel}
|
|
625
652
|
dragSourcePath=${dragSourcePath}
|
|
653
|
+
loadingFolderPaths=${loadingFolderPaths}
|
|
626
654
|
/>
|
|
627
655
|
`,
|
|
628
656
|
)}
|
|
@@ -650,6 +678,7 @@ export const FileTree = ({
|
|
|
650
678
|
const [creatingType, setCreatingType] = useState("");
|
|
651
679
|
const [contextMenu, setContextMenu] = useState(null);
|
|
652
680
|
const [dragSourcePath, setDragSourcePath] = useState("");
|
|
681
|
+
const [loadingFolderPaths, setLoadingFolderPaths] = useState(new Set());
|
|
653
682
|
const [selectedFolder, setSelectedFolder] = useState("");
|
|
654
683
|
const effectiveSelectedPath = selectedFolder || selectedPath;
|
|
655
684
|
const searchInputRef = useRef(null);
|
|
@@ -661,10 +690,31 @@ export const FileTree = ({
|
|
|
661
690
|
try {
|
|
662
691
|
const data = await fetchBrowseTree();
|
|
663
692
|
const nextRoot = data.root || null;
|
|
664
|
-
const
|
|
693
|
+
const nextExpandedPaths =
|
|
694
|
+
expandedPaths instanceof Set ? expandedPaths : new Set();
|
|
695
|
+
let hydratedRoot = nextRoot;
|
|
696
|
+
const hydratedPaths = new Set();
|
|
697
|
+
while (true) {
|
|
698
|
+
const truncatedExpandedPaths = [];
|
|
699
|
+
collectTruncatedExpandedFolderPaths(
|
|
700
|
+
hydratedRoot,
|
|
701
|
+
nextExpandedPaths,
|
|
702
|
+
truncatedExpandedPaths,
|
|
703
|
+
);
|
|
704
|
+
const nextFolderPath = truncatedExpandedPaths.find(
|
|
705
|
+
(folderPath) => !hydratedPaths.has(folderPath),
|
|
706
|
+
);
|
|
707
|
+
if (!nextFolderPath) break;
|
|
708
|
+
hydratedPaths.add(nextFolderPath);
|
|
709
|
+
const subtreeData = await fetchBrowseTree({ path: nextFolderPath });
|
|
710
|
+
if (subtreeData.root) {
|
|
711
|
+
hydratedRoot = replaceTreeNode(hydratedRoot, subtreeData.root);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const nextSignature = JSON.stringify(hydratedRoot || {});
|
|
665
715
|
if (treeSignatureRef.current !== nextSignature) {
|
|
666
716
|
treeSignatureRef.current = nextSignature;
|
|
667
|
-
setTreeRoot(
|
|
717
|
+
setTreeRoot(hydratedRoot);
|
|
668
718
|
}
|
|
669
719
|
setExpandedPaths((previousPaths) =>
|
|
670
720
|
previousPaths instanceof Set ? previousPaths : new Set(),
|
|
@@ -677,7 +727,7 @@ export const FileTree = ({
|
|
|
677
727
|
} finally {
|
|
678
728
|
if (showLoading) setLoading(false);
|
|
679
729
|
}
|
|
680
|
-
}, []);
|
|
730
|
+
}, [expandedPaths]);
|
|
681
731
|
|
|
682
732
|
useEffect(() => {
|
|
683
733
|
loadTree({ showLoading: true });
|
|
@@ -834,7 +884,31 @@ export const FileTree = ({
|
|
|
834
884
|
onPreviewFile("");
|
|
835
885
|
}, [isSearchActive, filteredFilePaths, searchActivePath, onPreviewFile]);
|
|
836
886
|
|
|
837
|
-
const setFolderExpanded = (folderPath, nextExpanded) => {
|
|
887
|
+
const setFolderExpanded = async (folderPath, nextExpanded, node = null) => {
|
|
888
|
+
if (nextExpanded === true && node?.truncated) {
|
|
889
|
+
setLoadingFolderPaths((previousPaths) => {
|
|
890
|
+
const nextPaths =
|
|
891
|
+
previousPaths instanceof Set ? new Set(previousPaths) : new Set();
|
|
892
|
+
nextPaths.add(folderPath);
|
|
893
|
+
return nextPaths;
|
|
894
|
+
});
|
|
895
|
+
try {
|
|
896
|
+
const data = await fetchBrowseTree({ path: folderPath });
|
|
897
|
+
if (data.root) {
|
|
898
|
+
setTreeRoot((previousRoot) => replaceTreeNode(previousRoot, data.root));
|
|
899
|
+
}
|
|
900
|
+
} catch (loadError) {
|
|
901
|
+
showToast(loadError.message || "Could not load folder", "error");
|
|
902
|
+
return;
|
|
903
|
+
} finally {
|
|
904
|
+
setLoadingFolderPaths((previousPaths) => {
|
|
905
|
+
const nextPaths =
|
|
906
|
+
previousPaths instanceof Set ? new Set(previousPaths) : new Set();
|
|
907
|
+
nextPaths.delete(folderPath);
|
|
908
|
+
return nextPaths;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
838
912
|
setExpandedPaths((previousPaths) => {
|
|
839
913
|
const nextPaths =
|
|
840
914
|
previousPaths instanceof Set ? new Set(previousPaths) : new Set();
|
|
@@ -1210,6 +1284,7 @@ export const FileTree = ({
|
|
|
1210
1284
|
onCreationConfirm=${confirmCreate}
|
|
1211
1285
|
onCreationCancel=${cancelCreate}
|
|
1212
1286
|
dragSourcePath=${dragSourcePath}
|
|
1287
|
+
loadingFolderPaths=${loadingFolderPaths}
|
|
1213
1288
|
/>
|
|
1214
1289
|
`,
|
|
1215
1290
|
)}
|
|
@@ -1245,6 +1320,7 @@ export const FileTree = ({
|
|
|
1245
1320
|
onCreationConfirm=${confirmCreate}
|
|
1246
1321
|
onCreationCancel=${cancelCreate}
|
|
1247
1322
|
dragSourcePath=${dragSourcePath}
|
|
1323
|
+
loadingFolderPaths=${loadingFolderPaths}
|
|
1248
1324
|
/>
|
|
1249
1325
|
`,
|
|
1250
1326
|
)}
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -1245,8 +1245,11 @@ export async function fetchWebhookRequest(name, id) {
|
|
|
1245
1245
|
return parseJsonOrThrow(res, "Could not load webhook request");
|
|
1246
1246
|
}
|
|
1247
1247
|
|
|
1248
|
-
export const fetchBrowseTree = async (
|
|
1248
|
+
export const fetchBrowseTree = async (options = {}) => {
|
|
1249
|
+
const { depth = 3, path = "" } =
|
|
1250
|
+
typeof options === "number" ? { depth: options, path: "" } : options;
|
|
1249
1251
|
const params = new URLSearchParams({ depth: String(depth) });
|
|
1252
|
+
if (path) params.set("path", String(path));
|
|
1250
1253
|
const res = await authFetch(`/api/browse/tree?${params.toString()}`);
|
|
1251
1254
|
return parseJsonOrThrow(res, "Could not load file tree");
|
|
1252
1255
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const kDefaultTreeDepth =
|
|
1
|
+
const kDefaultTreeDepth = 3;
|
|
2
|
+
const kMaxTreeDepth = 3;
|
|
2
3
|
const kIgnoredDirectoryNames = new Set([
|
|
3
4
|
".git",
|
|
4
5
|
".alphaclaw",
|
|
@@ -42,6 +43,7 @@ const kSqliteTablePageSize = 50;
|
|
|
42
43
|
|
|
43
44
|
module.exports = {
|
|
44
45
|
kDefaultTreeDepth,
|
|
46
|
+
kMaxTreeDepth,
|
|
45
47
|
kIgnoredDirectoryNames,
|
|
46
48
|
kImageMimeTypeByExtension,
|
|
47
49
|
kCommitHistoryLimit,
|
|
@@ -2,6 +2,7 @@ const path = require("path");
|
|
|
2
2
|
const { kLockedBrowsePaths, kProtectedBrowsePaths } = require("../../constants");
|
|
3
3
|
const {
|
|
4
4
|
kDefaultTreeDepth,
|
|
5
|
+
kMaxTreeDepth,
|
|
5
6
|
kIgnoredDirectoryNames,
|
|
6
7
|
kCommitHistoryLimit,
|
|
7
8
|
} = require("./constants");
|
|
@@ -34,6 +35,16 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
34
35
|
fs.mkdirSync(kRootResolved, { recursive: true });
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const readVisibleDirectoryEntries = (absolutePath) =>
|
|
39
|
+
fs
|
|
40
|
+
.readdirSync(absolutePath, { withFileTypes: true })
|
|
41
|
+
.filter((entry) => {
|
|
42
|
+
if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return entry.isDirectory() || entry.isFile();
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
const buildTreeNode = (absolutePath, depthRemaining) => {
|
|
38
49
|
const stats = fs.statSync(absolutePath);
|
|
39
50
|
const nodeName = path.basename(absolutePath);
|
|
@@ -44,17 +55,17 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
if (depthRemaining <= 0) {
|
|
47
|
-
|
|
58
|
+
const hasMoreChildren = readVisibleDirectoryEntries(absolutePath).length > 0;
|
|
59
|
+
return {
|
|
60
|
+
type: "folder",
|
|
61
|
+
name: nodeName,
|
|
62
|
+
path: nodePath,
|
|
63
|
+
children: [],
|
|
64
|
+
truncated: hasMoreChildren,
|
|
65
|
+
};
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
const children =
|
|
51
|
-
.readdirSync(absolutePath, { withFileTypes: true })
|
|
52
|
-
.filter((entry) => {
|
|
53
|
-
if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
return entry.isDirectory() || entry.isFile();
|
|
57
|
-
})
|
|
68
|
+
const children = readVisibleDirectoryEntries(absolutePath)
|
|
58
69
|
.map((entry) =>
|
|
59
70
|
buildTreeNode(path.join(absolutePath, entry.name), depthRemaining - 1),
|
|
60
71
|
)
|
|
@@ -70,12 +81,29 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
70
81
|
|
|
71
82
|
app.get("/api/browse/tree", (req, res) => {
|
|
72
83
|
const depthValue = Number.parseInt(String(req.query.depth || ""), 10);
|
|
73
|
-
const
|
|
84
|
+
const requestedDepth =
|
|
74
85
|
Number.isFinite(depthValue) && depthValue > 0
|
|
75
86
|
? depthValue
|
|
76
87
|
: kDefaultTreeDepth;
|
|
88
|
+
const depth = Math.min(requestedDepth, kMaxTreeDepth);
|
|
89
|
+
const requestedPath = String(req.query.path || "").trim();
|
|
77
90
|
try {
|
|
78
|
-
const
|
|
91
|
+
const resolvedPath = requestedPath
|
|
92
|
+
? resolveSafePath(
|
|
93
|
+
requestedPath,
|
|
94
|
+
kRootResolved,
|
|
95
|
+
kRootWithSep,
|
|
96
|
+
kRootDisplayName,
|
|
97
|
+
)
|
|
98
|
+
: { ok: true, absolutePath: kRootResolved };
|
|
99
|
+
if (!resolvedPath.ok) {
|
|
100
|
+
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
101
|
+
}
|
|
102
|
+
const stats = fs.statSync(resolvedPath.absolutePath);
|
|
103
|
+
if (!stats.isDirectory()) {
|
|
104
|
+
return res.status(400).json({ ok: false, error: "Path is not a folder" });
|
|
105
|
+
}
|
|
106
|
+
const tree = buildTreeNode(resolvedPath.absolutePath, depth);
|
|
79
107
|
return res.json({ ok: true, root: tree });
|
|
80
108
|
} catch (error) {
|
|
81
109
|
return res.status(500).json({
|