@chrysb/alphaclaw 0.3.2 → 0.3.3
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 +29 -2
- package/lib/cli/git-sync.js +25 -0
- package/lib/public/css/explorer.css +983 -0
- package/lib/public/css/shell.css +48 -4
- package/lib/public/css/theme.css +6 -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 +259 -158
- package/lib/public/js/components/action-button.js +12 -1
- package/lib/public/js/components/file-tree.js +322 -0
- package/lib/public/js/components/file-viewer.js +691 -0
- package/lib/public/js/components/icons.js +182 -0
- package/lib/public/js/components/sidebar-git-panel.js +149 -0
- package/lib/public/js/components/sidebar.js +272 -0
- package/lib/public/js/lib/api.js +26 -0
- 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/setup.html +1 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/workspace.js +3 -2
- package/lib/server/routes/browse.js +295 -0
- package/lib/server.js +24 -3
- package/lib/setup/core-prompts/TOOLS.md +3 -1
- package/lib/setup/skills/control-ui/SKILL.md +12 -20
- package/package.json +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, 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 getFileIconMeta = (fileName) => {
|
|
52
|
+
const normalizedName = String(fileName || "").toLowerCase();
|
|
53
|
+
if (normalizedName.endsWith(".md")) {
|
|
54
|
+
return {
|
|
55
|
+
icon: MarkdownFillIcon,
|
|
56
|
+
className: "file-icon file-icon-md",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (normalizedName.endsWith(".js") || normalizedName.endsWith(".mjs")) {
|
|
60
|
+
return {
|
|
61
|
+
icon: JavascriptFillIcon,
|
|
62
|
+
className: "file-icon file-icon-js",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (normalizedName.endsWith(".json") || normalizedName.endsWith(".jsonl")) {
|
|
66
|
+
return {
|
|
67
|
+
icon: BracesLineIcon,
|
|
68
|
+
className: "file-icon file-icon-json",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (normalizedName.endsWith(".css") || normalizedName.endsWith(".scss")) {
|
|
72
|
+
return {
|
|
73
|
+
icon: HashtagIcon,
|
|
74
|
+
className: "file-icon file-icon-css",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (/\.(html?)$/i.test(normalizedName)) {
|
|
78
|
+
return {
|
|
79
|
+
icon: FileCodeLineIcon,
|
|
80
|
+
className: "file-icon file-icon-html",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico|avif)$/i.test(normalizedName)) {
|
|
84
|
+
return {
|
|
85
|
+
icon: Image2FillIcon,
|
|
86
|
+
className: "file-icon file-icon-image",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
/\.(sh|bash|zsh|command)$/i.test(normalizedName) ||
|
|
91
|
+
[
|
|
92
|
+
".bashrc",
|
|
93
|
+
".zshrc",
|
|
94
|
+
".profile",
|
|
95
|
+
".bash_profile",
|
|
96
|
+
".zprofile",
|
|
97
|
+
".zshenv",
|
|
98
|
+
].includes(normalizedName)
|
|
99
|
+
) {
|
|
100
|
+
return {
|
|
101
|
+
icon: TerminalFillIcon,
|
|
102
|
+
className: "file-icon file-icon-shell",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
/\.(db|sqlite|sqlite3|db3|sdb|sqlitedb|duckdb|mdb|accdb)$/i.test(
|
|
107
|
+
normalizedName,
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
return {
|
|
111
|
+
icon: Database2LineIcon,
|
|
112
|
+
className: "file-icon file-icon-db",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
icon: File3LineIcon,
|
|
117
|
+
className: "file-icon file-icon-generic",
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const TreeNode = ({
|
|
122
|
+
node,
|
|
123
|
+
depth = 0,
|
|
124
|
+
collapsedPaths,
|
|
125
|
+
onToggleFolder,
|
|
126
|
+
onSelectFile,
|
|
127
|
+
selectedPath = "",
|
|
128
|
+
draftPaths,
|
|
129
|
+
}) => {
|
|
130
|
+
if (!node) return null;
|
|
131
|
+
if (node.type === "file") {
|
|
132
|
+
const isActive = selectedPath === node.path;
|
|
133
|
+
const hasDraft = draftPaths.has(node.path || "");
|
|
134
|
+
const fileIconMeta = getFileIconMeta(node.name);
|
|
135
|
+
const FileTypeIcon = fileIconMeta.icon;
|
|
136
|
+
return html`
|
|
137
|
+
<li class="tree-item">
|
|
138
|
+
<a
|
|
139
|
+
class=${isActive ? "active" : ""}
|
|
140
|
+
onclick=${() => onSelectFile(node.path)}
|
|
141
|
+
style=${{
|
|
142
|
+
paddingLeft: `${kFileBasePaddingPx + depth * kTreeIndentPx}px`,
|
|
143
|
+
}}
|
|
144
|
+
title=${node.path || node.name}
|
|
145
|
+
>
|
|
146
|
+
<${FileTypeIcon} className=${fileIconMeta.className} />
|
|
147
|
+
<span class="tree-label">${node.name}</span>
|
|
148
|
+
${hasDraft ? html`<span class="tree-draft-dot" aria-hidden="true"></span>` : null}
|
|
149
|
+
</a>
|
|
150
|
+
</li>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const folderPath = node.path || "";
|
|
155
|
+
const isCollapsed = collapsedPaths.has(folderPath);
|
|
156
|
+
return html`
|
|
157
|
+
<li class="tree-item">
|
|
158
|
+
<div
|
|
159
|
+
class=${`tree-folder ${isCollapsed ? "collapsed" : ""}`}
|
|
160
|
+
onclick=${() => onToggleFolder(folderPath)}
|
|
161
|
+
style=${{
|
|
162
|
+
paddingLeft: `${kFolderBasePaddingPx + depth * kTreeIndentPx}px`,
|
|
163
|
+
}}
|
|
164
|
+
title=${folderPath || node.name}
|
|
165
|
+
>
|
|
166
|
+
<span class="arrow">▼</span>
|
|
167
|
+
<span class="tree-label">${node.name}</span>
|
|
168
|
+
</div>
|
|
169
|
+
<ul class=${`tree-children ${isCollapsed ? "hidden" : ""}`}>
|
|
170
|
+
${(node.children || []).map(
|
|
171
|
+
(childNode) => html`
|
|
172
|
+
<${TreeNode}
|
|
173
|
+
key=${childNode.path || `${folderPath}/${childNode.name}`}
|
|
174
|
+
node=${childNode}
|
|
175
|
+
depth=${depth + 1}
|
|
176
|
+
collapsedPaths=${collapsedPaths}
|
|
177
|
+
onToggleFolder=${onToggleFolder}
|
|
178
|
+
onSelectFile=${onSelectFile}
|
|
179
|
+
selectedPath=${selectedPath}
|
|
180
|
+
draftPaths=${draftPaths}
|
|
181
|
+
/>
|
|
182
|
+
`,
|
|
183
|
+
)}
|
|
184
|
+
</ul>
|
|
185
|
+
</li>
|
|
186
|
+
`;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
190
|
+
const [treeRoot, setTreeRoot] = useState(null);
|
|
191
|
+
const [loading, setLoading] = useState(true);
|
|
192
|
+
const [error, setError] = useState("");
|
|
193
|
+
const [collapsedPaths, setCollapsedPaths] = useState(
|
|
194
|
+
readStoredCollapsedPaths,
|
|
195
|
+
);
|
|
196
|
+
const [draftPaths, setDraftPaths] = useState(readStoredDraftPaths);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
let active = true;
|
|
200
|
+
const loadTree = async () => {
|
|
201
|
+
setLoading(true);
|
|
202
|
+
setError("");
|
|
203
|
+
try {
|
|
204
|
+
const data = await fetchBrowseTree();
|
|
205
|
+
if (!active) return;
|
|
206
|
+
setTreeRoot(data.root || null);
|
|
207
|
+
setCollapsedPaths((previousPaths) => {
|
|
208
|
+
if (previousPaths instanceof Set) return previousPaths;
|
|
209
|
+
const nextPaths = new Set();
|
|
210
|
+
collectFolderPaths(data.root, nextPaths);
|
|
211
|
+
return nextPaths;
|
|
212
|
+
});
|
|
213
|
+
} catch (loadError) {
|
|
214
|
+
if (!active) return;
|
|
215
|
+
setError(loadError.message || "Could not load file tree");
|
|
216
|
+
} finally {
|
|
217
|
+
if (active) setLoading(false);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
loadTree();
|
|
221
|
+
return () => {
|
|
222
|
+
active = false;
|
|
223
|
+
};
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
const rootChildren = useMemo(() => treeRoot?.children || [], [treeRoot]);
|
|
227
|
+
const safeCollapsedPaths =
|
|
228
|
+
collapsedPaths instanceof Set ? collapsedPaths : new Set();
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!(collapsedPaths instanceof Set)) return;
|
|
232
|
+
try {
|
|
233
|
+
window.localStorage.setItem(
|
|
234
|
+
kCollapsedFoldersStorageKey,
|
|
235
|
+
JSON.stringify(Array.from(collapsedPaths)),
|
|
236
|
+
);
|
|
237
|
+
} catch {}
|
|
238
|
+
}, [collapsedPaths]);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!selectedPath) return;
|
|
242
|
+
const ancestorFolderPaths = collectAncestorFolderPaths(selectedPath);
|
|
243
|
+
if (!ancestorFolderPaths.length) return;
|
|
244
|
+
setCollapsedPaths((previousPaths) => {
|
|
245
|
+
if (!(previousPaths instanceof Set)) return previousPaths;
|
|
246
|
+
let didChange = false;
|
|
247
|
+
const nextPaths = new Set(previousPaths);
|
|
248
|
+
ancestorFolderPaths.forEach((ancestorPath) => {
|
|
249
|
+
if (nextPaths.has(ancestorPath)) {
|
|
250
|
+
nextPaths.delete(ancestorPath);
|
|
251
|
+
didChange = true;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return didChange ? nextPaths : previousPaths;
|
|
255
|
+
});
|
|
256
|
+
}, [selectedPath]);
|
|
257
|
+
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const handleDraftIndexChanged = (event) => {
|
|
260
|
+
const eventPaths = event?.detail?.paths;
|
|
261
|
+
if (Array.isArray(eventPaths)) {
|
|
262
|
+
setDraftPaths(
|
|
263
|
+
new Set(
|
|
264
|
+
eventPaths
|
|
265
|
+
.map((entry) => String(entry || "").trim())
|
|
266
|
+
.filter(Boolean),
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setDraftPaths(readStoredDraftPaths());
|
|
272
|
+
};
|
|
273
|
+
window.addEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
|
|
274
|
+
window.addEventListener("storage", handleDraftIndexChanged);
|
|
275
|
+
return () => {
|
|
276
|
+
window.removeEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
|
|
277
|
+
window.removeEventListener("storage", handleDraftIndexChanged);
|
|
278
|
+
};
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
const toggleFolder = (folderPath) => {
|
|
282
|
+
setCollapsedPaths((previousPaths) => {
|
|
283
|
+
const nextPaths =
|
|
284
|
+
previousPaths instanceof Set ? new Set(previousPaths) : new Set();
|
|
285
|
+
if (nextPaths.has(folderPath)) nextPaths.delete(folderPath);
|
|
286
|
+
else nextPaths.add(folderPath);
|
|
287
|
+
return nextPaths;
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (loading) {
|
|
292
|
+
return html`<div class="file-tree-state">Loading files...</div>`;
|
|
293
|
+
}
|
|
294
|
+
if (error) {
|
|
295
|
+
return html`<div class="file-tree-state file-tree-state-error">
|
|
296
|
+
${error}
|
|
297
|
+
</div>`;
|
|
298
|
+
}
|
|
299
|
+
if (!rootChildren.length) {
|
|
300
|
+
return html`<div class="file-tree-state">No files found.</div>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return html`
|
|
304
|
+
<div class="file-tree-wrap">
|
|
305
|
+
<ul class="file-tree">
|
|
306
|
+
${rootChildren.map(
|
|
307
|
+
(node) => html`
|
|
308
|
+
<${TreeNode}
|
|
309
|
+
key=${node.path || node.name}
|
|
310
|
+
node=${node}
|
|
311
|
+
collapsedPaths=${safeCollapsedPaths}
|
|
312
|
+
onToggleFolder=${toggleFolder}
|
|
313
|
+
onSelectFile=${onSelectFile}
|
|
314
|
+
selectedPath=${selectedPath}
|
|
315
|
+
draftPaths=${draftPaths}
|
|
316
|
+
/>
|
|
317
|
+
`,
|
|
318
|
+
)}
|
|
319
|
+
</ul>
|
|
320
|
+
</div>
|
|
321
|
+
`;
|
|
322
|
+
};
|