@chrysb/alphaclaw 0.3.2 → 0.3.4-beta.0
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/bin/alphaclaw.js +47 -2
- package/lib/cli/git-sync.js +25 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +1033 -0
- package/lib/public/css/shell.css +50 -4
- package/lib/public/css/theme.css +41 -1
- package/lib/public/icons/folder-line.svg +1 -0
- package/lib/public/icons/hashtag.svg +3 -0
- package/lib/public/icons/home-5-line.svg +1 -0
- package/lib/public/icons/save-fill.svg +3 -0
- package/lib/public/js/app.js +310 -160
- package/lib/public/js/components/action-button.js +12 -1
- package/lib/public/js/components/file-tree.js +497 -0
- package/lib/public/js/components/file-viewer.js +714 -0
- package/lib/public/js/components/icons.js +182 -0
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar-git-panel.js +149 -0
- package/lib/public/js/components/sidebar.js +254 -0
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +51 -1
- package/lib/public/js/lib/browse-draft-state.js +109 -0
- package/lib/public/js/lib/file-highlighting.js +6 -0
- package/lib/public/js/lib/file-tree-utils.js +12 -0
- package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
- package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
- package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
- package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
- package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
- package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
- package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -29
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +2 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/onboarding/workspace.js +3 -2
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/browse.js +295 -0
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +45 -4
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/lib/setup/core-prompts/TOOLS.md +3 -1
- package/lib/setup/skills/control-ui/SKILL.md +12 -20
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { fetchBrowseTree } from "../lib/api.js";
|
|
5
|
+
import {
|
|
6
|
+
kDraftIndexChangedEventName,
|
|
7
|
+
readStoredDraftPaths,
|
|
8
|
+
} from "../lib/browse-draft-state.js";
|
|
9
|
+
import { collectAncestorFolderPaths } from "../lib/file-tree-utils.js";
|
|
10
|
+
import {
|
|
11
|
+
MarkdownFillIcon,
|
|
12
|
+
JavascriptFillIcon,
|
|
13
|
+
File3LineIcon,
|
|
14
|
+
Image2FillIcon,
|
|
15
|
+
TerminalFillIcon,
|
|
16
|
+
BracesLineIcon,
|
|
17
|
+
FileCodeLineIcon,
|
|
18
|
+
Database2LineIcon,
|
|
19
|
+
HashtagIcon,
|
|
20
|
+
} from "./icons.js";
|
|
21
|
+
|
|
22
|
+
const html = htm.bind(h);
|
|
23
|
+
const kTreeIndentPx = 9;
|
|
24
|
+
const kFolderBasePaddingPx = 10;
|
|
25
|
+
const kFileBasePaddingPx = 14;
|
|
26
|
+
const kCollapsedFoldersStorageKey = "alphaclaw.browse.collapsedFolders";
|
|
27
|
+
const kLegacyCollapsedFoldersStorageKey = "alphaclawBrowseCollapsedFolders";
|
|
28
|
+
|
|
29
|
+
const readStoredCollapsedPaths = () => {
|
|
30
|
+
try {
|
|
31
|
+
const rawValue =
|
|
32
|
+
window.localStorage.getItem(kCollapsedFoldersStorageKey) ||
|
|
33
|
+
window.localStorage.getItem(kLegacyCollapsedFoldersStorageKey);
|
|
34
|
+
if (!rawValue) return null;
|
|
35
|
+
const parsedValue = JSON.parse(rawValue);
|
|
36
|
+
if (!Array.isArray(parsedValue)) return null;
|
|
37
|
+
return new Set(parsedValue.map((entry) => String(entry)));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const collectFolderPaths = (node, folderPaths) => {
|
|
44
|
+
if (!node || node.type !== "folder") return;
|
|
45
|
+
if (node.path) folderPaths.add(node.path);
|
|
46
|
+
(node.children || []).forEach((childNode) =>
|
|
47
|
+
collectFolderPaths(childNode, folderPaths),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const collectFilePaths = (node, filePaths) => {
|
|
52
|
+
if (!node) return;
|
|
53
|
+
if (node.type === "file") {
|
|
54
|
+
if (node.path) filePaths.push(node.path);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
(node.children || []).forEach((childNode) =>
|
|
58
|
+
collectFilePaths(childNode, filePaths),
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const filterTreeNode = (node, normalizedQuery) => {
|
|
63
|
+
if (!node) return null;
|
|
64
|
+
const query = String(normalizedQuery || "").trim().toLowerCase();
|
|
65
|
+
if (!query) return node;
|
|
66
|
+
const nodeName = String(node.name || "").toLowerCase();
|
|
67
|
+
const nodePath = String(node.path || "").toLowerCase();
|
|
68
|
+
const isDirectMatch = nodeName.includes(query) || nodePath.includes(query);
|
|
69
|
+
if (node.type === "file") {
|
|
70
|
+
return isDirectMatch ? node : null;
|
|
71
|
+
}
|
|
72
|
+
const filteredChildren = (node.children || [])
|
|
73
|
+
.map((childNode) => filterTreeNode(childNode, query))
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
if (!isDirectMatch && filteredChildren.length === 0) return null;
|
|
76
|
+
return {
|
|
77
|
+
...node,
|
|
78
|
+
children: filteredChildren,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getFileIconMeta = (fileName) => {
|
|
83
|
+
const normalizedName = String(fileName || "").toLowerCase();
|
|
84
|
+
if (normalizedName.endsWith(".md")) {
|
|
85
|
+
return {
|
|
86
|
+
icon: MarkdownFillIcon,
|
|
87
|
+
className: "file-icon file-icon-md",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (normalizedName.endsWith(".js") || normalizedName.endsWith(".mjs")) {
|
|
91
|
+
return {
|
|
92
|
+
icon: JavascriptFillIcon,
|
|
93
|
+
className: "file-icon file-icon-js",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (normalizedName.endsWith(".json") || normalizedName.endsWith(".jsonl")) {
|
|
97
|
+
return {
|
|
98
|
+
icon: BracesLineIcon,
|
|
99
|
+
className: "file-icon file-icon-json",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (normalizedName.endsWith(".css") || normalizedName.endsWith(".scss")) {
|
|
103
|
+
return {
|
|
104
|
+
icon: HashtagIcon,
|
|
105
|
+
className: "file-icon file-icon-css",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (/\.(html?)$/i.test(normalizedName)) {
|
|
109
|
+
return {
|
|
110
|
+
icon: FileCodeLineIcon,
|
|
111
|
+
className: "file-icon file-icon-html",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico|avif)$/i.test(normalizedName)) {
|
|
115
|
+
return {
|
|
116
|
+
icon: Image2FillIcon,
|
|
117
|
+
className: "file-icon file-icon-image",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
/\.(sh|bash|zsh|command)$/i.test(normalizedName) ||
|
|
122
|
+
[
|
|
123
|
+
".bashrc",
|
|
124
|
+
".zshrc",
|
|
125
|
+
".profile",
|
|
126
|
+
".bash_profile",
|
|
127
|
+
".zprofile",
|
|
128
|
+
".zshenv",
|
|
129
|
+
].includes(normalizedName)
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
icon: TerminalFillIcon,
|
|
133
|
+
className: "file-icon file-icon-shell",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
/\.(db|sqlite|sqlite3|db3|sdb|sqlitedb|duckdb|mdb|accdb)$/i.test(
|
|
138
|
+
normalizedName,
|
|
139
|
+
)
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
icon: Database2LineIcon,
|
|
143
|
+
className: "file-icon file-icon-db",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
icon: File3LineIcon,
|
|
148
|
+
className: "file-icon file-icon-generic",
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const TreeNode = ({
|
|
153
|
+
node,
|
|
154
|
+
depth = 0,
|
|
155
|
+
collapsedPaths,
|
|
156
|
+
onToggleFolder,
|
|
157
|
+
onSelectFile,
|
|
158
|
+
selectedPath = "",
|
|
159
|
+
draftPaths,
|
|
160
|
+
isSearchActive = false,
|
|
161
|
+
searchActivePath = "",
|
|
162
|
+
}) => {
|
|
163
|
+
if (!node) return null;
|
|
164
|
+
if (node.type === "file") {
|
|
165
|
+
const isActive = selectedPath === node.path;
|
|
166
|
+
const isSearchActiveNode = searchActivePath === node.path;
|
|
167
|
+
const hasDraft = draftPaths.has(node.path || "");
|
|
168
|
+
const fileIconMeta = getFileIconMeta(node.name);
|
|
169
|
+
const FileTypeIcon = fileIconMeta.icon;
|
|
170
|
+
return html`
|
|
171
|
+
<li class="tree-item">
|
|
172
|
+
<a
|
|
173
|
+
class=${`${isActive ? "active" : ""} ${isSearchActiveNode && !isActive ? "soft-active" : ""}`.trim()}
|
|
174
|
+
onclick=${() => onSelectFile(node.path)}
|
|
175
|
+
style=${{
|
|
176
|
+
paddingLeft: `${kFileBasePaddingPx + depth * kTreeIndentPx}px`,
|
|
177
|
+
}}
|
|
178
|
+
title=${node.path || node.name}
|
|
179
|
+
>
|
|
180
|
+
<${FileTypeIcon} className=${fileIconMeta.className} />
|
|
181
|
+
<span class="tree-label">${node.name}</span>
|
|
182
|
+
${hasDraft ? html`<span class="tree-draft-dot" aria-hidden="true"></span>` : null}
|
|
183
|
+
</a>
|
|
184
|
+
</li>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const folderPath = node.path || "";
|
|
189
|
+
const isCollapsed = isSearchActive ? false : collapsedPaths.has(folderPath);
|
|
190
|
+
return html`
|
|
191
|
+
<li class="tree-item">
|
|
192
|
+
<div
|
|
193
|
+
class=${`tree-folder ${isCollapsed ? "collapsed" : ""}`}
|
|
194
|
+
onclick=${() => onToggleFolder(folderPath)}
|
|
195
|
+
style=${{
|
|
196
|
+
paddingLeft: `${kFolderBasePaddingPx + depth * kTreeIndentPx}px`,
|
|
197
|
+
}}
|
|
198
|
+
title=${folderPath || node.name}
|
|
199
|
+
>
|
|
200
|
+
<span class="arrow">▼</span>
|
|
201
|
+
<span class="tree-label">${node.name}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<ul class=${`tree-children ${isCollapsed ? "hidden" : ""}`}>
|
|
204
|
+
${(node.children || []).map(
|
|
205
|
+
(childNode) => html`
|
|
206
|
+
<${TreeNode}
|
|
207
|
+
key=${childNode.path || `${folderPath}/${childNode.name}`}
|
|
208
|
+
node=${childNode}
|
|
209
|
+
depth=${depth + 1}
|
|
210
|
+
collapsedPaths=${collapsedPaths}
|
|
211
|
+
onToggleFolder=${onToggleFolder}
|
|
212
|
+
onSelectFile=${onSelectFile}
|
|
213
|
+
selectedPath=${selectedPath}
|
|
214
|
+
draftPaths=${draftPaths}
|
|
215
|
+
isSearchActive=${isSearchActive}
|
|
216
|
+
searchActivePath=${searchActivePath}
|
|
217
|
+
/>
|
|
218
|
+
`,
|
|
219
|
+
)}
|
|
220
|
+
</ul>
|
|
221
|
+
</li>
|
|
222
|
+
`;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const FileTree = ({
|
|
226
|
+
onSelectFile = () => {},
|
|
227
|
+
selectedPath = "",
|
|
228
|
+
onPreviewFile = () => {},
|
|
229
|
+
}) => {
|
|
230
|
+
const [treeRoot, setTreeRoot] = useState(null);
|
|
231
|
+
const [loading, setLoading] = useState(true);
|
|
232
|
+
const [error, setError] = useState("");
|
|
233
|
+
const [collapsedPaths, setCollapsedPaths] = useState(
|
|
234
|
+
readStoredCollapsedPaths,
|
|
235
|
+
);
|
|
236
|
+
const [draftPaths, setDraftPaths] = useState(readStoredDraftPaths);
|
|
237
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
238
|
+
const [searchActivePath, setSearchActivePath] = useState("");
|
|
239
|
+
const searchInputRef = useRef(null);
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
let active = true;
|
|
243
|
+
const loadTree = async () => {
|
|
244
|
+
setLoading(true);
|
|
245
|
+
setError("");
|
|
246
|
+
try {
|
|
247
|
+
const data = await fetchBrowseTree();
|
|
248
|
+
if (!active) return;
|
|
249
|
+
setTreeRoot(data.root || null);
|
|
250
|
+
setCollapsedPaths((previousPaths) => {
|
|
251
|
+
if (previousPaths instanceof Set) return previousPaths;
|
|
252
|
+
const nextPaths = new Set();
|
|
253
|
+
collectFolderPaths(data.root, nextPaths);
|
|
254
|
+
return nextPaths;
|
|
255
|
+
});
|
|
256
|
+
} catch (loadError) {
|
|
257
|
+
if (!active) return;
|
|
258
|
+
setError(loadError.message || "Could not load file tree");
|
|
259
|
+
} finally {
|
|
260
|
+
if (active) setLoading(false);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
loadTree();
|
|
264
|
+
return () => {
|
|
265
|
+
active = false;
|
|
266
|
+
};
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
const normalizedSearchQuery = String(searchQuery || "").trim().toLowerCase();
|
|
270
|
+
const rootChildren = useMemo(() => {
|
|
271
|
+
const children = treeRoot?.children || [];
|
|
272
|
+
if (!normalizedSearchQuery) return children;
|
|
273
|
+
return children
|
|
274
|
+
.map((node) => filterTreeNode(node, normalizedSearchQuery))
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
}, [treeRoot, normalizedSearchQuery]);
|
|
277
|
+
const safeCollapsedPaths =
|
|
278
|
+
collapsedPaths instanceof Set ? collapsedPaths : new Set();
|
|
279
|
+
const isSearchActive = normalizedSearchQuery.length > 0;
|
|
280
|
+
const filteredFilePaths = useMemo(() => {
|
|
281
|
+
const filePaths = [];
|
|
282
|
+
rootChildren.forEach((node) => collectFilePaths(node, filePaths));
|
|
283
|
+
return filePaths;
|
|
284
|
+
}, [rootChildren]);
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!(collapsedPaths instanceof Set)) return;
|
|
288
|
+
try {
|
|
289
|
+
window.localStorage.setItem(
|
|
290
|
+
kCollapsedFoldersStorageKey,
|
|
291
|
+
JSON.stringify(Array.from(collapsedPaths)),
|
|
292
|
+
);
|
|
293
|
+
} catch {}
|
|
294
|
+
}, [collapsedPaths]);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!selectedPath) return;
|
|
298
|
+
const ancestorFolderPaths = collectAncestorFolderPaths(selectedPath);
|
|
299
|
+
if (!ancestorFolderPaths.length) return;
|
|
300
|
+
setCollapsedPaths((previousPaths) => {
|
|
301
|
+
if (!(previousPaths instanceof Set)) return previousPaths;
|
|
302
|
+
let didChange = false;
|
|
303
|
+
const nextPaths = new Set(previousPaths);
|
|
304
|
+
ancestorFolderPaths.forEach((ancestorPath) => {
|
|
305
|
+
if (nextPaths.has(ancestorPath)) {
|
|
306
|
+
nextPaths.delete(ancestorPath);
|
|
307
|
+
didChange = true;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return didChange ? nextPaths : previousPaths;
|
|
311
|
+
});
|
|
312
|
+
}, [selectedPath]);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const handleDraftIndexChanged = (event) => {
|
|
316
|
+
const eventPaths = event?.detail?.paths;
|
|
317
|
+
if (Array.isArray(eventPaths)) {
|
|
318
|
+
setDraftPaths(
|
|
319
|
+
new Set(
|
|
320
|
+
eventPaths
|
|
321
|
+
.map((entry) => String(entry || "").trim())
|
|
322
|
+
.filter(Boolean),
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
setDraftPaths(readStoredDraftPaths());
|
|
328
|
+
};
|
|
329
|
+
window.addEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
|
|
330
|
+
window.addEventListener("storage", handleDraftIndexChanged);
|
|
331
|
+
return () => {
|
|
332
|
+
window.removeEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
|
|
333
|
+
window.removeEventListener("storage", handleDraftIndexChanged);
|
|
334
|
+
};
|
|
335
|
+
}, []);
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const handleGlobalSearchShortcut = (event) => {
|
|
339
|
+
if (event.key !== "/") return;
|
|
340
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
341
|
+
const target = event.target;
|
|
342
|
+
const tagName = String(target?.tagName || "").toLowerCase();
|
|
343
|
+
const isTypingTarget =
|
|
344
|
+
tagName === "input" ||
|
|
345
|
+
tagName === "textarea" ||
|
|
346
|
+
tagName === "select" ||
|
|
347
|
+
target?.isContentEditable;
|
|
348
|
+
if (isTypingTarget && target !== searchInputRef.current) return;
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
searchInputRef.current?.focus();
|
|
351
|
+
searchInputRef.current?.select();
|
|
352
|
+
};
|
|
353
|
+
window.addEventListener("keydown", handleGlobalSearchShortcut);
|
|
354
|
+
return () => {
|
|
355
|
+
window.removeEventListener("keydown", handleGlobalSearchShortcut);
|
|
356
|
+
};
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (!isSearchActive) {
|
|
361
|
+
setSearchActivePath("");
|
|
362
|
+
onPreviewFile("");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (searchActivePath && filteredFilePaths.includes(searchActivePath)) return;
|
|
366
|
+
setSearchActivePath("");
|
|
367
|
+
onPreviewFile("");
|
|
368
|
+
}, [isSearchActive, filteredFilePaths, searchActivePath, onPreviewFile]);
|
|
369
|
+
|
|
370
|
+
const toggleFolder = (folderPath) => {
|
|
371
|
+
setCollapsedPaths((previousPaths) => {
|
|
372
|
+
const nextPaths =
|
|
373
|
+
previousPaths instanceof Set ? new Set(previousPaths) : new Set();
|
|
374
|
+
if (nextPaths.has(folderPath)) nextPaths.delete(folderPath);
|
|
375
|
+
else nextPaths.add(folderPath);
|
|
376
|
+
return nextPaths;
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const updateSearchQuery = (nextQuery) => {
|
|
381
|
+
setSearchQuery(nextQuery);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const clearSearch = () => {
|
|
385
|
+
setSearchQuery("");
|
|
386
|
+
setSearchActivePath("");
|
|
387
|
+
onPreviewFile("");
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const moveSearchSelection = (direction) => {
|
|
391
|
+
if (!filteredFilePaths.length) return;
|
|
392
|
+
const currentIndex = filteredFilePaths.indexOf(searchActivePath);
|
|
393
|
+
const delta = direction === "up" ? -1 : 1;
|
|
394
|
+
const baseIndex = currentIndex === -1 ? (direction === "up" ? 0 : -1) : currentIndex;
|
|
395
|
+
const nextIndex =
|
|
396
|
+
(baseIndex + delta + filteredFilePaths.length) % filteredFilePaths.length;
|
|
397
|
+
const nextPath = filteredFilePaths[nextIndex];
|
|
398
|
+
setSearchActivePath(nextPath);
|
|
399
|
+
onPreviewFile(nextPath);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const commitSearchSelection = () => {
|
|
403
|
+
const [singlePath = ""] = filteredFilePaths;
|
|
404
|
+
const targetPath = searchActivePath || (filteredFilePaths.length === 1 ? singlePath : "");
|
|
405
|
+
if (!targetPath) return;
|
|
406
|
+
onSelectFile(targetPath);
|
|
407
|
+
clearSearch();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const onSearchKeyDown = (event) => {
|
|
411
|
+
if (event.key === "ArrowDown") {
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
moveSearchSelection("down");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (event.key === "ArrowUp") {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
moveSearchSelection("up");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (event.key === "Enter") {
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
commitSearchSelection();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (event.key === "Escape") {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
clearSearch();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
if (loading) {
|
|
433
|
+
return html`<div class="file-tree-state">Loading files...</div>`;
|
|
434
|
+
}
|
|
435
|
+
if (error) {
|
|
436
|
+
return html`<div class="file-tree-state file-tree-state-error">
|
|
437
|
+
${error}
|
|
438
|
+
</div>`;
|
|
439
|
+
}
|
|
440
|
+
if (!rootChildren.length) {
|
|
441
|
+
return html`
|
|
442
|
+
<div class="file-tree-wrap">
|
|
443
|
+
<div class="file-tree-search">
|
|
444
|
+
<input
|
|
445
|
+
class="file-tree-search-input"
|
|
446
|
+
type="text"
|
|
447
|
+
ref=${searchInputRef}
|
|
448
|
+
value=${searchQuery}
|
|
449
|
+
onInput=${(event) => updateSearchQuery(event.target.value)}
|
|
450
|
+
onKeyDown=${onSearchKeyDown}
|
|
451
|
+
placeholder="Search files..."
|
|
452
|
+
autocomplete="off"
|
|
453
|
+
spellcheck=${false}
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="file-tree-state">
|
|
457
|
+
${isSearchActive ? "No matching files." : "No files found."}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return html`
|
|
464
|
+
<div class="file-tree-wrap">
|
|
465
|
+
<div class="file-tree-search">
|
|
466
|
+
<input
|
|
467
|
+
class="file-tree-search-input"
|
|
468
|
+
type="text"
|
|
469
|
+
ref=${searchInputRef}
|
|
470
|
+
value=${searchQuery}
|
|
471
|
+
onInput=${(event) => updateSearchQuery(event.target.value)}
|
|
472
|
+
onKeyDown=${onSearchKeyDown}
|
|
473
|
+
placeholder="Search files..."
|
|
474
|
+
autocomplete="off"
|
|
475
|
+
spellcheck=${false}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
<ul class="file-tree">
|
|
479
|
+
${rootChildren.map(
|
|
480
|
+
(node) => html`
|
|
481
|
+
<${TreeNode}
|
|
482
|
+
key=${node.path || node.name}
|
|
483
|
+
node=${node}
|
|
484
|
+
collapsedPaths=${safeCollapsedPaths}
|
|
485
|
+
onToggleFolder=${toggleFolder}
|
|
486
|
+
onSelectFile=${onSelectFile}
|
|
487
|
+
selectedPath=${selectedPath}
|
|
488
|
+
draftPaths=${draftPaths}
|
|
489
|
+
isSearchActive=${isSearchActive}
|
|
490
|
+
searchActivePath=${searchActivePath}
|
|
491
|
+
/>
|
|
492
|
+
`,
|
|
493
|
+
)}
|
|
494
|
+
</ul>
|
|
495
|
+
</div>
|
|
496
|
+
`;
|
|
497
|
+
};
|