@chrysb/alphaclaw 0.3.3 → 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 +18 -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 +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +181 -6
- package/lib/public/js/components/file-viewer.js +43 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- 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 +25 -1
- 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 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- 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 +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { fetchBrowseTree } from "../lib/api.js";
|
|
5
5
|
import {
|
|
@@ -48,6 +48,37 @@ const collectFolderPaths = (node, folderPaths) => {
|
|
|
48
48
|
);
|
|
49
49
|
};
|
|
50
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
|
+
|
|
51
82
|
const getFileIconMeta = (fileName) => {
|
|
52
83
|
const normalizedName = String(fileName || "").toLowerCase();
|
|
53
84
|
if (normalizedName.endsWith(".md")) {
|
|
@@ -126,17 +157,20 @@ const TreeNode = ({
|
|
|
126
157
|
onSelectFile,
|
|
127
158
|
selectedPath = "",
|
|
128
159
|
draftPaths,
|
|
160
|
+
isSearchActive = false,
|
|
161
|
+
searchActivePath = "",
|
|
129
162
|
}) => {
|
|
130
163
|
if (!node) return null;
|
|
131
164
|
if (node.type === "file") {
|
|
132
165
|
const isActive = selectedPath === node.path;
|
|
166
|
+
const isSearchActiveNode = searchActivePath === node.path;
|
|
133
167
|
const hasDraft = draftPaths.has(node.path || "");
|
|
134
168
|
const fileIconMeta = getFileIconMeta(node.name);
|
|
135
169
|
const FileTypeIcon = fileIconMeta.icon;
|
|
136
170
|
return html`
|
|
137
171
|
<li class="tree-item">
|
|
138
172
|
<a
|
|
139
|
-
class=${isActive ? "active" : ""}
|
|
173
|
+
class=${`${isActive ? "active" : ""} ${isSearchActiveNode && !isActive ? "soft-active" : ""}`.trim()}
|
|
140
174
|
onclick=${() => onSelectFile(node.path)}
|
|
141
175
|
style=${{
|
|
142
176
|
paddingLeft: `${kFileBasePaddingPx + depth * kTreeIndentPx}px`,
|
|
@@ -152,7 +186,7 @@ const TreeNode = ({
|
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
const folderPath = node.path || "";
|
|
155
|
-
const isCollapsed = collapsedPaths.has(folderPath);
|
|
189
|
+
const isCollapsed = isSearchActive ? false : collapsedPaths.has(folderPath);
|
|
156
190
|
return html`
|
|
157
191
|
<li class="tree-item">
|
|
158
192
|
<div
|
|
@@ -178,6 +212,8 @@ const TreeNode = ({
|
|
|
178
212
|
onSelectFile=${onSelectFile}
|
|
179
213
|
selectedPath=${selectedPath}
|
|
180
214
|
draftPaths=${draftPaths}
|
|
215
|
+
isSearchActive=${isSearchActive}
|
|
216
|
+
searchActivePath=${searchActivePath}
|
|
181
217
|
/>
|
|
182
218
|
`,
|
|
183
219
|
)}
|
|
@@ -186,7 +222,11 @@ const TreeNode = ({
|
|
|
186
222
|
`;
|
|
187
223
|
};
|
|
188
224
|
|
|
189
|
-
export const FileTree = ({
|
|
225
|
+
export const FileTree = ({
|
|
226
|
+
onSelectFile = () => {},
|
|
227
|
+
selectedPath = "",
|
|
228
|
+
onPreviewFile = () => {},
|
|
229
|
+
}) => {
|
|
190
230
|
const [treeRoot, setTreeRoot] = useState(null);
|
|
191
231
|
const [loading, setLoading] = useState(true);
|
|
192
232
|
const [error, setError] = useState("");
|
|
@@ -194,6 +234,9 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
194
234
|
readStoredCollapsedPaths,
|
|
195
235
|
);
|
|
196
236
|
const [draftPaths, setDraftPaths] = useState(readStoredDraftPaths);
|
|
237
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
238
|
+
const [searchActivePath, setSearchActivePath] = useState("");
|
|
239
|
+
const searchInputRef = useRef(null);
|
|
197
240
|
|
|
198
241
|
useEffect(() => {
|
|
199
242
|
let active = true;
|
|
@@ -223,9 +266,22 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
223
266
|
};
|
|
224
267
|
}, []);
|
|
225
268
|
|
|
226
|
-
const
|
|
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]);
|
|
227
277
|
const safeCollapsedPaths =
|
|
228
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]);
|
|
229
285
|
|
|
230
286
|
useEffect(() => {
|
|
231
287
|
if (!(collapsedPaths instanceof Set)) return;
|
|
@@ -278,6 +334,39 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
278
334
|
};
|
|
279
335
|
}, []);
|
|
280
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
|
+
|
|
281
370
|
const toggleFolder = (folderPath) => {
|
|
282
371
|
setCollapsedPaths((previousPaths) => {
|
|
283
372
|
const nextPaths =
|
|
@@ -288,6 +377,58 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
288
377
|
});
|
|
289
378
|
};
|
|
290
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
|
+
|
|
291
432
|
if (loading) {
|
|
292
433
|
return html`<div class="file-tree-state">Loading files...</div>`;
|
|
293
434
|
}
|
|
@@ -297,11 +438,43 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
297
438
|
</div>`;
|
|
298
439
|
}
|
|
299
440
|
if (!rootChildren.length) {
|
|
300
|
-
return html
|
|
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
|
+
`;
|
|
301
461
|
}
|
|
302
462
|
|
|
303
463
|
return html`
|
|
304
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>
|
|
305
478
|
<ul class="file-tree">
|
|
306
479
|
${rootChildren.map(
|
|
307
480
|
(node) => html`
|
|
@@ -313,6 +486,8 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
|
|
|
313
486
|
onSelectFile=${onSelectFile}
|
|
314
487
|
selectedPath=${selectedPath}
|
|
315
488
|
draftPaths=${draftPaths}
|
|
489
|
+
isSearchActive=${isSearchActive}
|
|
490
|
+
searchActivePath=${searchActivePath}
|
|
316
491
|
/>
|
|
317
492
|
`,
|
|
318
493
|
)}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
writeStoredFileDraft,
|
|
23
23
|
} from "../lib/browse-draft-state.js";
|
|
24
24
|
import { ActionButton } from "./action-button.js";
|
|
25
|
+
import { LoadingSpinner } from "./loading-spinner.js";
|
|
26
|
+
import { SegmentedControl } from "./segmented-control.js";
|
|
25
27
|
import { SaveFillIcon } from "./icons.js";
|
|
26
28
|
import { showToast } from "./toast.js";
|
|
27
29
|
|
|
@@ -29,6 +31,7 @@ const html = htm.bind(h);
|
|
|
29
31
|
const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
|
|
30
32
|
const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
|
|
31
33
|
const kProtectedBrowsePaths = new Set(["openclaw.json", "devices/paired.json"]);
|
|
34
|
+
const kLoadingIndicatorDelayMs = 1000;
|
|
32
35
|
|
|
33
36
|
const parsePathSegments = (inputPath) =>
|
|
34
37
|
String(inputPath || "")
|
|
@@ -58,13 +61,18 @@ const readStoredFileViewerMode = () => {
|
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
|
|
61
|
-
export const FileViewer = ({
|
|
64
|
+
export const FileViewer = ({
|
|
65
|
+
filePath = "",
|
|
66
|
+
isPreviewOnly = false,
|
|
67
|
+
}) => {
|
|
62
68
|
const normalizedPath = String(filePath || "").trim();
|
|
63
69
|
const normalizedPolicyPath = normalizePolicyPath(normalizedPath);
|
|
64
70
|
const [content, setContent] = useState("");
|
|
65
71
|
const [initialContent, setInitialContent] = useState("");
|
|
66
72
|
const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
|
|
67
73
|
const [loading, setLoading] = useState(false);
|
|
74
|
+
const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] =
|
|
75
|
+
useState(false);
|
|
68
76
|
const [saving, setSaving] = useState(false);
|
|
69
77
|
const [error, setError] = useState("");
|
|
70
78
|
const [isFolderPath, setIsFolderPath] = useState(false);
|
|
@@ -87,7 +95,7 @@ export const FileViewer = ({ filePath = "" }) => {
|
|
|
87
95
|
[normalizedPath],
|
|
88
96
|
);
|
|
89
97
|
const hasSelectedPath = normalizedPath.length > 0;
|
|
90
|
-
const canEditFile = hasSelectedPath && !isFolderPath;
|
|
98
|
+
const canEditFile = hasSelectedPath && !isFolderPath && !isPreviewOnly;
|
|
91
99
|
const isDirty = canEditFile && content !== initialContent;
|
|
92
100
|
const isProtectedFile =
|
|
93
101
|
canEditFile && kProtectedBrowsePaths.has(normalizedPolicyPath);
|
|
@@ -164,6 +172,17 @@ export const FileViewer = ({ filePath = "" }) => {
|
|
|
164
172
|
} catch {}
|
|
165
173
|
}, [viewMode]);
|
|
166
174
|
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!loading) {
|
|
177
|
+
setShowDelayedLoadingSpinner(false);
|
|
178
|
+
return () => {};
|
|
179
|
+
}
|
|
180
|
+
const timer = window.setTimeout(() => {
|
|
181
|
+
setShowDelayedLoadingSpinner(true);
|
|
182
|
+
}, kLoadingIndicatorDelayMs);
|
|
183
|
+
return () => window.clearTimeout(timer);
|
|
184
|
+
}, [loading]);
|
|
185
|
+
|
|
167
186
|
useEffect(() => {
|
|
168
187
|
let active = true;
|
|
169
188
|
loadedFilePathRef.current = "";
|
|
@@ -282,7 +301,7 @@ export const FileViewer = ({ filePath = "" }) => {
|
|
|
282
301
|
};
|
|
283
302
|
|
|
284
303
|
const handleContentInput = (event) => {
|
|
285
|
-
if (isProtectedLocked) return;
|
|
304
|
+
if (isProtectedLocked || isPreviewOnly) return;
|
|
286
305
|
const nextContent = event.target.value;
|
|
287
306
|
setContent(nextContent);
|
|
288
307
|
if (hasSelectedPath && canEditFile) {
|
|
@@ -408,22 +427,20 @@ export const FileViewer = ({ filePath = "" }) => {
|
|
|
408
427
|
: null}
|
|
409
428
|
</div>
|
|
410
429
|
<div class="file-viewer-tabbar-spacer"></div>
|
|
430
|
+
${isPreviewOnly
|
|
431
|
+
? html`<div class="file-viewer-preview-pill">Preview</div>`
|
|
432
|
+
: null}
|
|
411
433
|
${isMarkdownFile &&
|
|
412
434
|
html`
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
onclick=${() => handleChangeViewMode("preview")}
|
|
423
|
-
>
|
|
424
|
-
preview
|
|
425
|
-
</button>
|
|
426
|
-
</div>
|
|
435
|
+
<${SegmentedControl}
|
|
436
|
+
className="mr-2.5"
|
|
437
|
+
options=${[
|
|
438
|
+
{ label: "edit", value: "edit" },
|
|
439
|
+
{ label: "preview", value: "preview" },
|
|
440
|
+
]}
|
|
441
|
+
value=${viewMode}
|
|
442
|
+
onChange=${handleChangeViewMode}
|
|
443
|
+
/>
|
|
427
444
|
`}
|
|
428
445
|
<${ActionButton}
|
|
429
446
|
onClick=${handleSave}
|
|
@@ -508,7 +525,13 @@ ${formattedValue}</pre
|
|
|
508
525
|
`
|
|
509
526
|
: null}
|
|
510
527
|
${loading
|
|
511
|
-
? html
|
|
528
|
+
? html`
|
|
529
|
+
<div class="file-viewer-loading-shell">
|
|
530
|
+
${showDelayedLoadingSpinner
|
|
531
|
+
? html`<${LoadingSpinner} className="h-4 w-4" />`
|
|
532
|
+
: null}
|
|
533
|
+
</div>
|
|
534
|
+
`
|
|
512
535
|
: error
|
|
513
536
|
? html`<div class="file-viewer-state file-viewer-state-error">
|
|
514
537
|
${error}
|
|
@@ -653,7 +676,7 @@ ${formattedValue}</pre
|
|
|
653
676
|
value=${content}
|
|
654
677
|
onInput=${handleContentInput}
|
|
655
678
|
onScroll=${handleEditorScroll}
|
|
656
|
-
readonly=${isProtectedLocked}
|
|
679
|
+
readonly=${isProtectedLocked || isPreviewOnly}
|
|
657
680
|
spellcheck=${false}
|
|
658
681
|
autocorrect="off"
|
|
659
682
|
autocapitalize="off"
|
|
@@ -672,7 +695,7 @@ ${formattedValue}</pre
|
|
|
672
695
|
value=${content}
|
|
673
696
|
onInput=${handleContentInput}
|
|
674
697
|
onScroll=${handleEditorScroll}
|
|
675
|
-
readonly=${isProtectedLocked}
|
|
698
|
+
readonly=${isProtectedLocked || isPreviewOnly}
|
|
676
699
|
spellcheck=${false}
|
|
677
700
|
autocorrect="off"
|
|
678
701
|
autocapitalize="off"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reusable segmented control (pill toggle).
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} props
|
|
10
|
+
* @param {Array<{label:string, value:*}>} props.options
|
|
11
|
+
* @param {*} props.value Currently selected value.
|
|
12
|
+
* @param {Function} props.onChange Called with the new value on click.
|
|
13
|
+
* @param {string} [props.className] Extra classes on the wrapper.
|
|
14
|
+
*/
|
|
15
|
+
export const SegmentedControl = ({
|
|
16
|
+
options = [],
|
|
17
|
+
value,
|
|
18
|
+
onChange = () => {},
|
|
19
|
+
className = "",
|
|
20
|
+
}) => html`
|
|
21
|
+
<div class=${`ac-segmented-control ${className}`}>
|
|
22
|
+
${options.map(
|
|
23
|
+
(option) => html`
|
|
24
|
+
<button
|
|
25
|
+
class=${`ac-segmented-control-button ${option.value === value ? "active" : ""}`}
|
|
26
|
+
onclick=${() => onChange(option.value)}
|
|
27
|
+
>
|
|
28
|
+
${option.label}
|
|
29
|
+
</button>
|
|
30
|
+
`,
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
@@ -5,37 +5,26 @@ import { HomeLineIcon, FolderLineIcon } from "./icons.js";
|
|
|
5
5
|
import { FileTree } from "./file-tree.js";
|
|
6
6
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
7
7
|
import { SidebarGitPanel } from "./sidebar-git-panel.js";
|
|
8
|
+
import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
|
|
8
9
|
|
|
9
10
|
const html = htm.bind(h);
|
|
10
|
-
const kUiSettingsStorageKey = "alphaclaw.uiSettings";
|
|
11
|
-
const kLegacyUiSettingsStorageKey = "alphaclawUiSettings";
|
|
12
11
|
const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
|
|
13
12
|
const kBrowsePanelMinHeightPx = 120;
|
|
14
13
|
const kBrowseBottomMinHeightPx = 120;
|
|
15
14
|
const kBrowseResizerHeightPx = 6;
|
|
16
15
|
const kDefaultBrowseBottomPanelHeightPx = 160;
|
|
17
|
-
const kLegacyBrowsePanelUiStorageKey = "alphaclawBrowsePanelHeightPx";
|
|
18
16
|
|
|
19
17
|
const readStoredBrowseBottomPanelHeight = () => {
|
|
20
18
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
10,
|
|
29
|
-
);
|
|
30
|
-
if (Number.isFinite(fromSharedSettings) && fromSharedSettings > 0) {
|
|
31
|
-
return fromSharedSettings;
|
|
32
|
-
}
|
|
19
|
+
const settings = readUiSettings();
|
|
20
|
+
const fromSharedSettings = Number.parseInt(
|
|
21
|
+
String(settings?.[kBrowseBottomPanelUiSettingKey] || ""),
|
|
22
|
+
10,
|
|
23
|
+
);
|
|
24
|
+
if (Number.isFinite(fromSharedSettings) && fromSharedSettings > 0) {
|
|
25
|
+
return fromSharedSettings;
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
const parsedValue = Number.parseInt(String(legacyRawValue || ""), 10);
|
|
36
|
-
return Number.isFinite(parsedValue) && parsedValue > 0
|
|
37
|
-
? parsedValue
|
|
38
|
-
: kDefaultBrowseBottomPanelHeightPx;
|
|
27
|
+
return kDefaultBrowseBottomPanelHeightPx;
|
|
39
28
|
} catch {
|
|
40
29
|
return kDefaultBrowseBottomPanelHeightPx;
|
|
41
30
|
}
|
|
@@ -55,6 +44,7 @@ export const AppSidebar = ({
|
|
|
55
44
|
onSelectNavItem = () => {},
|
|
56
45
|
selectedBrowsePath = "",
|
|
57
46
|
onSelectBrowseFile = () => {},
|
|
47
|
+
onPreviewBrowseFile = () => {},
|
|
58
48
|
acHasUpdate = false,
|
|
59
49
|
acLatest = "",
|
|
60
50
|
acDismissed = false,
|
|
@@ -70,18 +60,9 @@ export const AppSidebar = ({
|
|
|
70
60
|
const [isResizingBrowsePanels, setIsResizingBrowsePanels] = useState(false);
|
|
71
61
|
|
|
72
62
|
useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
window.localStorage.getItem(kLegacyUiSettingsStorageKey);
|
|
77
|
-
const parsedSettings = rawSettings ? JSON.parse(rawSettings) : {};
|
|
78
|
-
const nextSettings =
|
|
79
|
-
parsedSettings && typeof parsedSettings === "object"
|
|
80
|
-
? { ...parsedSettings }
|
|
81
|
-
: {};
|
|
82
|
-
nextSettings[kBrowseBottomPanelUiSettingKey] = browseBottomPanelHeightPx;
|
|
83
|
-
window.localStorage.setItem(kUiSettingsStorageKey, JSON.stringify(nextSettings));
|
|
84
|
-
} catch {}
|
|
63
|
+
const settings = readUiSettings();
|
|
64
|
+
settings[kBrowseBottomPanelUiSettingKey] = browseBottomPanelHeightPx;
|
|
65
|
+
writeUiSettings(settings);
|
|
85
66
|
}, [browseBottomPanelHeightPx]);
|
|
86
67
|
|
|
87
68
|
const getClampedBrowseBottomPanelHeight = (value) => {
|
|
@@ -217,6 +198,7 @@ export const AppSidebar = ({
|
|
|
217
198
|
<${FileTree}
|
|
218
199
|
onSelectFile=${onSelectBrowseFile}
|
|
219
200
|
selectedPath=${selectedBrowsePath}
|
|
201
|
+
onPreviewFile=${onPreviewBrowseFile}
|
|
220
202
|
/>
|
|
221
203
|
</div>
|
|
222
204
|
<div
|