@chrysb/alphaclaw 0.3.5-beta.0 → 0.3.5-beta.1
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 +65 -1
- package/lib/public/css/explorer.css +201 -6
- package/lib/public/js/app.js +45 -1
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/file-tree.js +56 -67
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +164 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +59 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +77 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +83 -30
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +72 -11
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -0
- package/lib/public/js/lib/api.js +16 -0
- package/lib/public/js/lib/browse-file-policies.js +34 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +8 -0
- package/lib/server/routes/browse/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/{browse.js → browse/index.js} +290 -218
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -1095
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { marked } from "https://esm.sh/marked";
|
|
3
|
+
import { saveFileContent } from "../../lib/api.js";
|
|
4
|
+
import {
|
|
5
|
+
getFileSyntaxKind,
|
|
6
|
+
highlightEditorLines,
|
|
7
|
+
parseFrontmatter,
|
|
8
|
+
} from "../../lib/syntax-highlighters/index.js";
|
|
9
|
+
import {
|
|
10
|
+
clearStoredFileDraft,
|
|
11
|
+
updateDraftIndex,
|
|
12
|
+
writeStoredFileDraft,
|
|
13
|
+
} from "../../lib/browse-draft-state.js";
|
|
14
|
+
import {
|
|
15
|
+
kLockedBrowsePaths,
|
|
16
|
+
kProtectedBrowsePaths,
|
|
17
|
+
matchesBrowsePolicyPath,
|
|
18
|
+
normalizeBrowsePolicyPath,
|
|
19
|
+
} from "../../lib/browse-file-policies.js";
|
|
20
|
+
import { showToast } from "../toast.js";
|
|
21
|
+
import { kFileViewerModeStorageKey, kLoadingIndicatorDelayMs } from "./constants.js";
|
|
22
|
+
import { readStoredFileViewerMode, writeStoredEditorSelection } from "./storage.js";
|
|
23
|
+
import { parsePathSegments } from "./utils.js";
|
|
24
|
+
import { useScrollSync } from "./scroll-sync.js";
|
|
25
|
+
import { useFileLoader } from "./use-file-loader.js";
|
|
26
|
+
import { useFileDiff } from "./use-file-diff.js";
|
|
27
|
+
import { useFileViewerDraftSync } from "./use-file-viewer-draft-sync.js";
|
|
28
|
+
import { useFileViewerHotkeys } from "./use-file-viewer-hotkeys.js";
|
|
29
|
+
import { useEditorSelectionRestore } from "./use-editor-selection-restore.js";
|
|
30
|
+
|
|
31
|
+
export const useFileViewer = ({
|
|
32
|
+
filePath = "",
|
|
33
|
+
isPreviewOnly = false,
|
|
34
|
+
browseView = "edit",
|
|
35
|
+
}) => {
|
|
36
|
+
const normalizedPath = String(filePath || "").trim();
|
|
37
|
+
const normalizedPolicyPath = normalizeBrowsePolicyPath(normalizedPath);
|
|
38
|
+
const [content, setContent] = useState("");
|
|
39
|
+
const [initialContent, setInitialContent] = useState("");
|
|
40
|
+
const [fileKind, setFileKind] = useState("text");
|
|
41
|
+
const [imageDataUrl, setImageDataUrl] = useState("");
|
|
42
|
+
const [audioDataUrl, setAudioDataUrl] = useState("");
|
|
43
|
+
const [sqliteSummary, setSqliteSummary] = useState(null);
|
|
44
|
+
const [sqliteSelectedTable, setSqliteSelectedTable] = useState("");
|
|
45
|
+
const [sqliteTableOffset, setSqliteTableOffset] = useState(0);
|
|
46
|
+
const [sqliteTableLoading, setSqliteTableLoading] = useState(false);
|
|
47
|
+
const [sqliteTableError, setSqliteTableError] = useState("");
|
|
48
|
+
const [sqliteTableData, setSqliteTableData] = useState(null);
|
|
49
|
+
const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
|
|
50
|
+
const [loading, setLoading] = useState(false);
|
|
51
|
+
const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] = useState(false);
|
|
52
|
+
const [saving, setSaving] = useState(false);
|
|
53
|
+
const [error, setError] = useState("");
|
|
54
|
+
const [isFolderPath, setIsFolderPath] = useState(false);
|
|
55
|
+
const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
|
|
56
|
+
const [externalChangeNoticeShown, setExternalChangeNoticeShown] = useState(false);
|
|
57
|
+
const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(() => new Set());
|
|
58
|
+
const editorLineNumbersRef = useRef(null);
|
|
59
|
+
const editorHighlightRef = useRef(null);
|
|
60
|
+
const editorTextareaRef = useRef(null);
|
|
61
|
+
const previewRef = useRef(null);
|
|
62
|
+
const editorLineNumberRowRefs = useRef([]);
|
|
63
|
+
const editorHighlightLineRefs = useRef([]);
|
|
64
|
+
|
|
65
|
+
const hasSelectedPath = normalizedPath.length > 0;
|
|
66
|
+
const isImageFile = fileKind === "image";
|
|
67
|
+
const isAudioFile = fileKind === "audio";
|
|
68
|
+
const isSqliteFile = fileKind === "sqlite";
|
|
69
|
+
const canEditFile =
|
|
70
|
+
hasSelectedPath && !isFolderPath && !isPreviewOnly && !isImageFile && !isAudioFile && !isSqliteFile;
|
|
71
|
+
const isDiffView = String(browseView || "edit") === "diff";
|
|
72
|
+
|
|
73
|
+
const { viewScrollRatioRef, handleEditorScroll, handlePreviewScroll, handleChangeViewMode } =
|
|
74
|
+
useScrollSync({
|
|
75
|
+
viewMode,
|
|
76
|
+
setViewMode,
|
|
77
|
+
previewRef,
|
|
78
|
+
editorTextareaRef,
|
|
79
|
+
editorLineNumbersRef,
|
|
80
|
+
editorHighlightRef,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { loadedFilePathRef, restoredSelectionPathRef } = useFileLoader({
|
|
84
|
+
hasSelectedPath,
|
|
85
|
+
normalizedPath,
|
|
86
|
+
isSqliteFile,
|
|
87
|
+
sqliteSelectedTable,
|
|
88
|
+
sqliteTableOffset,
|
|
89
|
+
canEditFile,
|
|
90
|
+
isFolderPath,
|
|
91
|
+
loading,
|
|
92
|
+
saving,
|
|
93
|
+
initialContent,
|
|
94
|
+
isDirty: canEditFile && content !== initialContent,
|
|
95
|
+
setLoading,
|
|
96
|
+
setContent,
|
|
97
|
+
setInitialContent,
|
|
98
|
+
setFileKind,
|
|
99
|
+
setImageDataUrl,
|
|
100
|
+
setAudioDataUrl,
|
|
101
|
+
setSqliteSummary,
|
|
102
|
+
setSqliteSelectedTable,
|
|
103
|
+
setSqliteTableOffset,
|
|
104
|
+
setSqliteTableLoading,
|
|
105
|
+
setSqliteTableError,
|
|
106
|
+
setSqliteTableData,
|
|
107
|
+
setError,
|
|
108
|
+
setIsFolderPath,
|
|
109
|
+
setExternalChangeNoticeShown,
|
|
110
|
+
externalChangeNoticeShown,
|
|
111
|
+
viewScrollRatioRef,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const { diffLoading, diffError, diffContent } = useFileDiff({
|
|
115
|
+
hasSelectedPath,
|
|
116
|
+
isDiffView,
|
|
117
|
+
isPreviewOnly,
|
|
118
|
+
normalizedPath,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const pathSegments = useMemo(() => parsePathSegments(normalizedPath), [normalizedPath]);
|
|
122
|
+
const isCurrentFileLoaded = loadedFilePathRef.current === normalizedPath;
|
|
123
|
+
const renderContent = isCurrentFileLoaded ? content : "";
|
|
124
|
+
const renderInitialContent = isCurrentFileLoaded ? initialContent : "";
|
|
125
|
+
const isDirty = canEditFile && renderContent !== renderInitialContent;
|
|
126
|
+
const isLockedFile =
|
|
127
|
+
canEditFile && matchesBrowsePolicyPath(kLockedBrowsePaths, normalizedPolicyPath);
|
|
128
|
+
const isProtectedFile =
|
|
129
|
+
canEditFile &&
|
|
130
|
+
!isLockedFile &&
|
|
131
|
+
matchesBrowsePolicyPath(kProtectedBrowsePaths, normalizedPolicyPath);
|
|
132
|
+
const isProtectedLocked = isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
|
|
133
|
+
const isEditBlocked = isLockedFile || isProtectedLocked;
|
|
134
|
+
const syntaxKind = useMemo(() => getFileSyntaxKind(normalizedPath), [normalizedPath]);
|
|
135
|
+
const isMarkdownFile = syntaxKind === "markdown";
|
|
136
|
+
const shouldUseHighlightedEditor = syntaxKind !== "plain";
|
|
137
|
+
const parsedFrontmatter = useMemo(
|
|
138
|
+
() => (isMarkdownFile ? parseFrontmatter(renderContent) : { entries: [], body: renderContent }),
|
|
139
|
+
[renderContent, isMarkdownFile],
|
|
140
|
+
);
|
|
141
|
+
const highlightedEditorLines = useMemo(
|
|
142
|
+
() => (shouldUseHighlightedEditor ? highlightEditorLines(renderContent, syntaxKind) : []),
|
|
143
|
+
[renderContent, shouldUseHighlightedEditor, syntaxKind],
|
|
144
|
+
);
|
|
145
|
+
const editorLineNumbers = useMemo(() => {
|
|
146
|
+
const lineCount = String(renderContent || "").split("\n").length;
|
|
147
|
+
return Array.from({ length: lineCount }, (_, index) => index + 1);
|
|
148
|
+
}, [renderContent]);
|
|
149
|
+
const previewHtml = useMemo(
|
|
150
|
+
() =>
|
|
151
|
+
isMarkdownFile
|
|
152
|
+
? marked.parse(parsedFrontmatter.body || "", {
|
|
153
|
+
gfm: true,
|
|
154
|
+
breaks: true,
|
|
155
|
+
})
|
|
156
|
+
: "",
|
|
157
|
+
[parsedFrontmatter.body, isMarkdownFile],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const syncEditorLineNumberHeights = useCallback(() => {
|
|
161
|
+
if (!shouldUseHighlightedEditor || viewMode !== "edit") return;
|
|
162
|
+
const numberRows = editorLineNumberRowRefs.current;
|
|
163
|
+
const highlightRows = editorHighlightLineRefs.current;
|
|
164
|
+
const rowCount = Math.min(numberRows.length, highlightRows.length);
|
|
165
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
166
|
+
const numberRow = numberRows[index];
|
|
167
|
+
const highlightRow = highlightRows[index];
|
|
168
|
+
if (!numberRow || !highlightRow) continue;
|
|
169
|
+
numberRow.style.height = `${highlightRow.offsetHeight}px`;
|
|
170
|
+
}
|
|
171
|
+
}, [shouldUseHighlightedEditor, viewMode]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
syncEditorLineNumberHeights();
|
|
175
|
+
}, [content, syncEditorLineNumberHeights]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!shouldUseHighlightedEditor || viewMode !== "edit") return () => {};
|
|
179
|
+
const onResize = () => syncEditorLineNumberHeights();
|
|
180
|
+
window.addEventListener("resize", onResize);
|
|
181
|
+
return () => window.removeEventListener("resize", onResize);
|
|
182
|
+
}, [shouldUseHighlightedEditor, viewMode, syncEditorLineNumberHeights]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!isMarkdownFile && viewMode !== "edit") {
|
|
186
|
+
setViewMode("edit");
|
|
187
|
+
}
|
|
188
|
+
}, [isMarkdownFile, viewMode]);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
try {
|
|
192
|
+
window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
|
|
193
|
+
} catch {}
|
|
194
|
+
}, [viewMode]);
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!loading) {
|
|
198
|
+
setShowDelayedLoadingSpinner(false);
|
|
199
|
+
return () => {};
|
|
200
|
+
}
|
|
201
|
+
const timer = window.setTimeout(() => {
|
|
202
|
+
setShowDelayedLoadingSpinner(true);
|
|
203
|
+
}, kLoadingIndicatorDelayMs);
|
|
204
|
+
return () => window.clearTimeout(timer);
|
|
205
|
+
}, [loading]);
|
|
206
|
+
|
|
207
|
+
useFileViewerDraftSync({
|
|
208
|
+
loadedFilePathRef,
|
|
209
|
+
normalizedPath,
|
|
210
|
+
canEditFile,
|
|
211
|
+
hasSelectedPath,
|
|
212
|
+
loading,
|
|
213
|
+
content,
|
|
214
|
+
initialContent,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
useEditorSelectionRestore({
|
|
218
|
+
canEditFile,
|
|
219
|
+
loading,
|
|
220
|
+
hasSelectedPath,
|
|
221
|
+
normalizedPath,
|
|
222
|
+
loadedFilePathRef,
|
|
223
|
+
restoredSelectionPathRef,
|
|
224
|
+
viewMode,
|
|
225
|
+
content,
|
|
226
|
+
editorTextareaRef,
|
|
227
|
+
editorLineNumbersRef,
|
|
228
|
+
editorHighlightRef,
|
|
229
|
+
viewScrollRatioRef,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const handleSave = useCallback(async () => {
|
|
233
|
+
if (!canEditFile || saving || !isDirty || isEditBlocked) return;
|
|
234
|
+
setSaving(true);
|
|
235
|
+
setError("");
|
|
236
|
+
try {
|
|
237
|
+
await saveFileContent(normalizedPath, content);
|
|
238
|
+
setInitialContent(content);
|
|
239
|
+
setExternalChangeNoticeShown(false);
|
|
240
|
+
clearStoredFileDraft(normalizedPath);
|
|
241
|
+
updateDraftIndex(normalizedPath, false, {
|
|
242
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
243
|
+
});
|
|
244
|
+
window.dispatchEvent(
|
|
245
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
246
|
+
detail: { path: normalizedPath },
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
showToast("Saved", "success");
|
|
250
|
+
} catch (saveError) {
|
|
251
|
+
const message = saveError.message || "Could not save file";
|
|
252
|
+
setError(message);
|
|
253
|
+
showToast(message, "error");
|
|
254
|
+
} finally {
|
|
255
|
+
setSaving(false);
|
|
256
|
+
}
|
|
257
|
+
}, [canEditFile, saving, isDirty, isEditBlocked, normalizedPath, content]);
|
|
258
|
+
|
|
259
|
+
useFileViewerHotkeys({
|
|
260
|
+
canEditFile,
|
|
261
|
+
isPreviewOnly,
|
|
262
|
+
isDiffView,
|
|
263
|
+
viewMode,
|
|
264
|
+
handleSave,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const handleEditProtectedFile = () => {
|
|
268
|
+
if (!normalizedPolicyPath) return;
|
|
269
|
+
setProtectedEditBypassPaths((previousPaths) => {
|
|
270
|
+
const nextPaths = new Set(previousPaths);
|
|
271
|
+
nextPaths.add(normalizedPolicyPath);
|
|
272
|
+
return nextPaths;
|
|
273
|
+
});
|
|
274
|
+
window.requestAnimationFrame(() => {
|
|
275
|
+
window.requestAnimationFrame(() => {
|
|
276
|
+
const textareaElement = editorTextareaRef.current;
|
|
277
|
+
if (!textareaElement) return;
|
|
278
|
+
if (textareaElement.disabled || textareaElement.readOnly) return;
|
|
279
|
+
textareaElement.focus();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const handleContentInput = (event) => {
|
|
285
|
+
if (isEditBlocked || isPreviewOnly) return;
|
|
286
|
+
const nextContent = event.target.value;
|
|
287
|
+
setContent(nextContent);
|
|
288
|
+
if (hasSelectedPath && canEditFile) {
|
|
289
|
+
writeStoredEditorSelection(normalizedPath, {
|
|
290
|
+
start: event.target.selectionStart,
|
|
291
|
+
end: event.target.selectionEnd,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (hasSelectedPath && canEditFile) {
|
|
295
|
+
writeStoredFileDraft(normalizedPath, nextContent);
|
|
296
|
+
updateDraftIndex(normalizedPath, nextContent !== initialContent, {
|
|
297
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleEditorSelectionChange = () => {
|
|
303
|
+
if (!hasSelectedPath || !canEditFile || loading) return;
|
|
304
|
+
const textareaElement = editorTextareaRef.current;
|
|
305
|
+
if (!textareaElement) return;
|
|
306
|
+
writeStoredEditorSelection(normalizedPath, {
|
|
307
|
+
start: textareaElement.selectionStart,
|
|
308
|
+
end: textareaElement.selectionEnd,
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
state: {
|
|
314
|
+
hasSelectedPath,
|
|
315
|
+
isPreviewOnly,
|
|
316
|
+
loading,
|
|
317
|
+
saving,
|
|
318
|
+
showDelayedLoadingSpinner,
|
|
319
|
+
error,
|
|
320
|
+
isFolderPath,
|
|
321
|
+
isImageFile,
|
|
322
|
+
imageDataUrl,
|
|
323
|
+
isAudioFile,
|
|
324
|
+
audioDataUrl,
|
|
325
|
+
isSqliteFile,
|
|
326
|
+
sqliteSummary,
|
|
327
|
+
sqliteSelectedTable,
|
|
328
|
+
sqliteTableOffset,
|
|
329
|
+
sqliteTableLoading,
|
|
330
|
+
sqliteTableError,
|
|
331
|
+
sqliteTableData,
|
|
332
|
+
isDiffView,
|
|
333
|
+
diffLoading,
|
|
334
|
+
diffError,
|
|
335
|
+
diffContent,
|
|
336
|
+
isMarkdownFile,
|
|
337
|
+
frontmatterCollapsed,
|
|
338
|
+
previewHtml,
|
|
339
|
+
viewMode,
|
|
340
|
+
renderContent,
|
|
341
|
+
},
|
|
342
|
+
derived: {
|
|
343
|
+
pathSegments,
|
|
344
|
+
isDirty,
|
|
345
|
+
canEditFile,
|
|
346
|
+
isEditBlocked,
|
|
347
|
+
isLockedFile,
|
|
348
|
+
isProtectedFile,
|
|
349
|
+
isProtectedLocked,
|
|
350
|
+
shouldUseHighlightedEditor,
|
|
351
|
+
parsedFrontmatter,
|
|
352
|
+
highlightedEditorLines,
|
|
353
|
+
editorLineNumbers,
|
|
354
|
+
},
|
|
355
|
+
refs: {
|
|
356
|
+
previewRef,
|
|
357
|
+
editorLineNumbersRef,
|
|
358
|
+
editorLineNumberRowRefs,
|
|
359
|
+
editorHighlightRef,
|
|
360
|
+
editorHighlightLineRefs,
|
|
361
|
+
editorTextareaRef,
|
|
362
|
+
},
|
|
363
|
+
actions: {
|
|
364
|
+
setFrontmatterCollapsed,
|
|
365
|
+
setSqliteSelectedTable,
|
|
366
|
+
setSqliteTableOffset,
|
|
367
|
+
handleChangeViewMode,
|
|
368
|
+
handleSave,
|
|
369
|
+
handleEditProtectedFile,
|
|
370
|
+
handleContentInput,
|
|
371
|
+
handleEditorScroll,
|
|
372
|
+
handlePreviewScroll,
|
|
373
|
+
handleEditorSelectionChange,
|
|
374
|
+
},
|
|
375
|
+
context: {
|
|
376
|
+
normalizedPath,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const parsePathSegments = (inputPath) =>
|
|
2
|
+
String(inputPath || "")
|
|
3
|
+
.split("/")
|
|
4
|
+
.map((part) => part.trim())
|
|
5
|
+
.filter(Boolean);
|
|
6
|
+
|
|
7
|
+
export const clampSelectionIndex = (value, maxValue) => {
|
|
8
|
+
const numericValue = Number.parseInt(String(value ?? ""), 10);
|
|
9
|
+
if (!Number.isFinite(numericValue)) return 0;
|
|
10
|
+
return Math.max(0, Math.min(maxValue, numericValue));
|
|
11
|
+
};
|
|
@@ -24,7 +24,14 @@ const formatDuration = (ms) => {
|
|
|
24
24
|
return `${seconds}s`;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const VersionRow = ({
|
|
28
|
+
label,
|
|
29
|
+
currentVersion,
|
|
30
|
+
fetchVersion,
|
|
31
|
+
applyUpdate,
|
|
32
|
+
updateInProgress = false,
|
|
33
|
+
onActionComplete = () => {},
|
|
34
|
+
}) => {
|
|
28
35
|
const [checking, setChecking] = useState(false);
|
|
29
36
|
const [version, setVersion] = useState(currentVersion || null);
|
|
30
37
|
const [latestVersion, setLatestVersion] = useState(null);
|
|
@@ -51,6 +58,10 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
51
58
|
})();
|
|
52
59
|
const effectiveHasUpdate = simulateUpdate || hasUpdate;
|
|
53
60
|
const effectiveLatestVersion = simulatedVersion || latestVersion;
|
|
61
|
+
const isUpdateActionActive = updateInProgress || effectiveHasUpdate;
|
|
62
|
+
const updateIdleLabel = effectiveLatestVersion
|
|
63
|
+
? `Update to ${effectiveLatestVersion}`
|
|
64
|
+
: "Update";
|
|
54
65
|
const changelogUrl = "https://github.com/openclaw/openclaw/tags";
|
|
55
66
|
const showMobileUpdateRow = effectiveHasUpdate && effectiveLatestVersion;
|
|
56
67
|
|
|
@@ -60,9 +71,9 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
60
71
|
|
|
61
72
|
useEffect(() => {
|
|
62
73
|
let active = true;
|
|
63
|
-
const load = async () => {
|
|
74
|
+
const load = async (refresh = false) => {
|
|
64
75
|
try {
|
|
65
|
-
const data = await fetchVersion(
|
|
76
|
+
const data = await fetchVersion(refresh);
|
|
66
77
|
if (!active) return;
|
|
67
78
|
setVersion(data.currentVersion || currentVersion || null);
|
|
68
79
|
setLatestVersion(data.latestVersion || null);
|
|
@@ -73,11 +84,30 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
73
84
|
setError(err.message || "Could not check updates");
|
|
74
85
|
}
|
|
75
86
|
};
|
|
76
|
-
load();
|
|
87
|
+
load(false);
|
|
77
88
|
return () => {
|
|
78
89
|
active = false;
|
|
79
90
|
};
|
|
80
|
-
}, []);
|
|
91
|
+
}, [currentVersion, fetchVersion]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (updateInProgress) return () => {};
|
|
95
|
+
let active = true;
|
|
96
|
+
const timeoutId = setTimeout(async () => {
|
|
97
|
+
try {
|
|
98
|
+
const data = await fetchVersion(true);
|
|
99
|
+
if (!active) return;
|
|
100
|
+
setVersion(data.currentVersion || currentVersion || null);
|
|
101
|
+
setLatestVersion(data.latestVersion || null);
|
|
102
|
+
setHasUpdate(!!data.hasUpdate);
|
|
103
|
+
setError(data.ok ? "" : data.error || "");
|
|
104
|
+
} catch {}
|
|
105
|
+
}, 1200);
|
|
106
|
+
return () => {
|
|
107
|
+
active = false;
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
};
|
|
110
|
+
}, [updateInProgress, currentVersion, fetchVersion]);
|
|
81
111
|
|
|
82
112
|
useEffect(() => {
|
|
83
113
|
if (!effectiveHasUpdate || !effectiveLatestVersion) {
|
|
@@ -88,16 +118,18 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
88
118
|
}, [effectiveHasUpdate, effectiveLatestVersion]);
|
|
89
119
|
|
|
90
120
|
const runAction = async () => {
|
|
91
|
-
|
|
121
|
+
const isUpdateAction = !!effectiveHasUpdate;
|
|
122
|
+
const busy = isUpdateActionActive ? checking || updateInProgress : checking;
|
|
123
|
+
if (busy) return;
|
|
92
124
|
setChecking(true);
|
|
93
125
|
setError("");
|
|
94
126
|
try {
|
|
95
|
-
const data =
|
|
127
|
+
const data = isUpdateAction ? await applyUpdate() : await fetchVersion(true);
|
|
96
128
|
setVersion(data.currentVersion || version);
|
|
97
129
|
setLatestVersion(data.latestVersion || null);
|
|
98
130
|
setHasUpdate(!!data.hasUpdate);
|
|
99
131
|
setError(data.ok ? "" : data.error || "");
|
|
100
|
-
if (
|
|
132
|
+
if (isUpdateAction) {
|
|
101
133
|
if (!data.ok) {
|
|
102
134
|
showToast(data.error || `${label} update failed`, "error");
|
|
103
135
|
} else if (data.updated || data.restarting) {
|
|
@@ -118,21 +150,33 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
118
150
|
} else {
|
|
119
151
|
showToast(`${label} is up to date`, "success");
|
|
120
152
|
}
|
|
153
|
+
await onActionComplete({
|
|
154
|
+
type: isUpdateAction ? "update" : "check",
|
|
155
|
+
ok: !!data?.ok,
|
|
156
|
+
result: data,
|
|
157
|
+
});
|
|
121
158
|
} catch (err) {
|
|
122
159
|
setError(
|
|
123
160
|
err.message ||
|
|
124
|
-
(
|
|
161
|
+
(isUpdateAction ? `Could not update ${label}` : "Could not check updates"),
|
|
125
162
|
);
|
|
126
163
|
showToast(
|
|
127
|
-
|
|
164
|
+
isUpdateAction ? `Could not update ${label}` : "Could not check updates",
|
|
128
165
|
"error",
|
|
129
166
|
);
|
|
167
|
+
await onActionComplete({
|
|
168
|
+
type: isUpdateAction ? "update" : "check",
|
|
169
|
+
ok: false,
|
|
170
|
+
error: err,
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
setChecking(false);
|
|
130
174
|
}
|
|
131
|
-
setChecking(false);
|
|
132
175
|
};
|
|
133
176
|
|
|
134
177
|
const handleAction = () => {
|
|
135
|
-
|
|
178
|
+
const busy = isUpdateActionActive ? checking || updateInProgress : checking;
|
|
179
|
+
if (busy) return;
|
|
136
180
|
if (effectiveHasUpdate && effectiveLatestVersion && !hasViewedChangelog) {
|
|
137
181
|
setConfirmWithoutChangelogOpen(true);
|
|
138
182
|
return;
|
|
@@ -145,6 +189,10 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
145
189
|
runAction();
|
|
146
190
|
};
|
|
147
191
|
|
|
192
|
+
const updateButtonLoading = isUpdateActionActive
|
|
193
|
+
? checking || updateInProgress
|
|
194
|
+
: checking;
|
|
195
|
+
|
|
148
196
|
return html`
|
|
149
197
|
<div class="flex items-center justify-between gap-3">
|
|
150
198
|
<div class="min-w-0">
|
|
@@ -175,24 +223,24 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
175
223
|
? html`
|
|
176
224
|
<${UpdateActionButton}
|
|
177
225
|
onClick=${handleAction}
|
|
178
|
-
loading=${
|
|
179
|
-
warning=${
|
|
180
|
-
idleLabel=${
|
|
181
|
-
?
|
|
226
|
+
loading=${updateButtonLoading}
|
|
227
|
+
warning=${isUpdateActionActive}
|
|
228
|
+
idleLabel=${isUpdateActionActive
|
|
229
|
+
? updateIdleLabel
|
|
182
230
|
: "Check updates"}
|
|
183
|
-
loadingLabel=${
|
|
231
|
+
loadingLabel=${isUpdateActionActive ? "Updating..." : "Checking..."}
|
|
184
232
|
className="hidden md:inline-flex"
|
|
185
233
|
/>
|
|
186
234
|
`
|
|
187
235
|
: html`
|
|
188
236
|
<${UpdateActionButton}
|
|
189
237
|
onClick=${handleAction}
|
|
190
|
-
loading=${
|
|
191
|
-
warning=${
|
|
192
|
-
idleLabel=${
|
|
193
|
-
?
|
|
238
|
+
loading=${updateButtonLoading}
|
|
239
|
+
warning=${isUpdateActionActive}
|
|
240
|
+
idleLabel=${isUpdateActionActive
|
|
241
|
+
? updateIdleLabel
|
|
194
242
|
: "Check updates"}
|
|
195
|
-
loadingLabel=${
|
|
243
|
+
loadingLabel=${isUpdateActionActive ? "Updating..." : "Checking..."}
|
|
196
244
|
/>
|
|
197
245
|
`}
|
|
198
246
|
</div>
|
|
@@ -209,9 +257,9 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
209
257
|
>
|
|
210
258
|
<${UpdateActionButton}
|
|
211
259
|
onClick=${handleAction}
|
|
212
|
-
loading=${
|
|
213
|
-
warning=${
|
|
214
|
-
idleLabel=${
|
|
260
|
+
loading=${updateButtonLoading}
|
|
261
|
+
warning=${isUpdateActionActive}
|
|
262
|
+
idleLabel=${updateIdleLabel}
|
|
215
263
|
loadingLabel="Updating..."
|
|
216
264
|
className="flex-1 h-9 px-3"
|
|
217
265
|
/>
|
|
@@ -228,9 +276,9 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
228
276
|
onConfirm=${handleConfirmWithoutChangelog}
|
|
229
277
|
/>
|
|
230
278
|
`;
|
|
231
|
-
}
|
|
279
|
+
};
|
|
232
280
|
|
|
233
|
-
export
|
|
281
|
+
export const Gateway = ({
|
|
234
282
|
status,
|
|
235
283
|
openclawVersion,
|
|
236
284
|
restarting = false,
|
|
@@ -239,7 +287,10 @@ export function Gateway({
|
|
|
239
287
|
onOpenWatchdog,
|
|
240
288
|
onRepair,
|
|
241
289
|
repairing = false,
|
|
242
|
-
|
|
290
|
+
openclawUpdateInProgress = false,
|
|
291
|
+
onOpenclawVersionActionComplete = () => {},
|
|
292
|
+
onOpenclawUpdate = updateOpenclaw,
|
|
293
|
+
}) => {
|
|
243
294
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
244
295
|
const isRunning = status === "running" && !restarting;
|
|
245
296
|
const dotClass = isRunning
|
|
@@ -357,8 +408,10 @@ export function Gateway({
|
|
|
357
408
|
label="OpenClaw"
|
|
358
409
|
currentVersion=${openclawVersion}
|
|
359
410
|
fetchVersion=${fetchOpenclawVersion}
|
|
360
|
-
applyUpdate=${
|
|
411
|
+
applyUpdate=${onOpenclawUpdate}
|
|
412
|
+
updateInProgress=${openclawUpdateInProgress}
|
|
413
|
+
onActionComplete=${onOpenclawVersionActionComplete}
|
|
361
414
|
/>
|
|
362
415
|
</div>
|
|
363
416
|
</div>`;
|
|
364
|
-
}
|
|
417
|
+
};
|
|
@@ -119,6 +119,19 @@ export const Image2FillIcon = ({ className = "" }) => html`
|
|
|
119
119
|
</svg>
|
|
120
120
|
`;
|
|
121
121
|
|
|
122
|
+
export const FileMusicLineIcon = ({ className = "" }) => html`
|
|
123
|
+
<svg
|
|
124
|
+
class=${className}
|
|
125
|
+
viewBox="0 0 24 24"
|
|
126
|
+
fill="currentColor"
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
d="M16 8V10H13V14.5C13 15.8807 11.8807 17 10.5 17C9.11929 17 8 15.8807 8 14.5C8 13.1193 9.11929 12 10.5 12C10.6712 12 10.8384 12.0172 11 12.05V8H15V4H5V20H19V8H16ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
`;
|
|
134
|
+
|
|
122
135
|
export const TerminalFillIcon = ({ className = "" }) => html`
|
|
123
136
|
<svg
|
|
124
137
|
class=${className}
|