@chrysb/alphaclaw 0.3.5-beta.0 → 0.4.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 +66 -32
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +254 -6
- package/lib/public/js/app.js +165 -100
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +267 -88
- 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 +202 -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 +64 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +119 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +93 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +60 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +312 -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 +471 -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/google/account-row.js +98 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/index.js +439 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +39 -0
- package/lib/public/js/components/sidebar-git-panel.js +115 -25
- package/lib/public/js/components/sidebar.js +91 -75
- 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 +88 -8
- package/lib/public/js/lib/browse-file-policies.js +52 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +20 -0
- package/lib/server/google-state.js +187 -0
- package/lib/server/helpers.js +12 -4
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -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/index.js +660 -0
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/google.js +414 -213
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +28 -1
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -1095
- package/lib/public/js/components/google.js +0 -228
- package/lib/server/routes/browse.js +0 -500
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { marked } from "https://esm.sh/marked";
|
|
3
|
+
import { deleteBrowseFile, restoreBrowseFile, 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
|
+
onRequestClearSelection = () => {},
|
|
36
|
+
onRequestEdit = () => {},
|
|
37
|
+
}) => {
|
|
38
|
+
const normalizedPath = String(filePath || "").trim();
|
|
39
|
+
const normalizedPolicyPath = normalizeBrowsePolicyPath(normalizedPath);
|
|
40
|
+
const [content, setContent] = useState("");
|
|
41
|
+
const [initialContent, setInitialContent] = useState("");
|
|
42
|
+
const [fileKind, setFileKind] = useState("text");
|
|
43
|
+
const [imageDataUrl, setImageDataUrl] = useState("");
|
|
44
|
+
const [audioDataUrl, setAudioDataUrl] = useState("");
|
|
45
|
+
const [sqliteSummary, setSqliteSummary] = useState(null);
|
|
46
|
+
const [sqliteSelectedTable, setSqliteSelectedTable] = useState("");
|
|
47
|
+
const [sqliteTableOffset, setSqliteTableOffset] = useState(0);
|
|
48
|
+
const [sqliteTableLoading, setSqliteTableLoading] = useState(false);
|
|
49
|
+
const [sqliteTableError, setSqliteTableError] = useState("");
|
|
50
|
+
const [sqliteTableData, setSqliteTableData] = useState(null);
|
|
51
|
+
const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
|
|
52
|
+
const [loading, setLoading] = useState(false);
|
|
53
|
+
const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] = useState(false);
|
|
54
|
+
const [saving, setSaving] = useState(false);
|
|
55
|
+
const [deleting, setDeleting] = useState(false);
|
|
56
|
+
const [restoring, setRestoring] = useState(false);
|
|
57
|
+
const [error, setError] = useState("");
|
|
58
|
+
const [isFolderPath, setIsFolderPath] = useState(false);
|
|
59
|
+
const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
|
|
60
|
+
const [externalChangeNoticeShown, setExternalChangeNoticeShown] = useState(false);
|
|
61
|
+
const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(() => new Set());
|
|
62
|
+
const editorLineNumbersRef = useRef(null);
|
|
63
|
+
const editorHighlightRef = useRef(null);
|
|
64
|
+
const editorTextareaRef = useRef(null);
|
|
65
|
+
const previewRef = useRef(null);
|
|
66
|
+
const editorLineNumberRowRefs = useRef([]);
|
|
67
|
+
const editorHighlightLineRefs = useRef([]);
|
|
68
|
+
|
|
69
|
+
const hasSelectedPath = normalizedPath.length > 0;
|
|
70
|
+
const isImageFile = fileKind === "image";
|
|
71
|
+
const isAudioFile = fileKind === "audio";
|
|
72
|
+
const isSqliteFile = fileKind === "sqlite";
|
|
73
|
+
const canEditFile =
|
|
74
|
+
hasSelectedPath && !isFolderPath && !isPreviewOnly && !isImageFile && !isAudioFile && !isSqliteFile;
|
|
75
|
+
const isDiffView = String(browseView || "edit") === "diff";
|
|
76
|
+
|
|
77
|
+
const { viewScrollRatioRef, handleEditorScroll, handlePreviewScroll, handleChangeViewMode } =
|
|
78
|
+
useScrollSync({
|
|
79
|
+
viewMode,
|
|
80
|
+
setViewMode,
|
|
81
|
+
previewRef,
|
|
82
|
+
editorTextareaRef,
|
|
83
|
+
editorLineNumbersRef,
|
|
84
|
+
editorHighlightRef,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { loadedFilePathRef, restoredSelectionPathRef } = useFileLoader({
|
|
88
|
+
hasSelectedPath,
|
|
89
|
+
normalizedPath,
|
|
90
|
+
isDiffView,
|
|
91
|
+
isSqliteFile,
|
|
92
|
+
sqliteSelectedTable,
|
|
93
|
+
sqliteTableOffset,
|
|
94
|
+
canEditFile,
|
|
95
|
+
isFolderPath,
|
|
96
|
+
loading,
|
|
97
|
+
saving,
|
|
98
|
+
initialContent,
|
|
99
|
+
isDirty: canEditFile && content !== initialContent,
|
|
100
|
+
setLoading,
|
|
101
|
+
setContent,
|
|
102
|
+
setInitialContent,
|
|
103
|
+
setFileKind,
|
|
104
|
+
setImageDataUrl,
|
|
105
|
+
setAudioDataUrl,
|
|
106
|
+
setSqliteSummary,
|
|
107
|
+
setSqliteSelectedTable,
|
|
108
|
+
setSqliteTableOffset,
|
|
109
|
+
setSqliteTableLoading,
|
|
110
|
+
setSqliteTableError,
|
|
111
|
+
setSqliteTableData,
|
|
112
|
+
setError,
|
|
113
|
+
setIsFolderPath,
|
|
114
|
+
setExternalChangeNoticeShown,
|
|
115
|
+
externalChangeNoticeShown,
|
|
116
|
+
viewScrollRatioRef,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const { diffLoading, diffError, diffContent, diffStatus } = useFileDiff({
|
|
120
|
+
hasSelectedPath,
|
|
121
|
+
isDiffView,
|
|
122
|
+
isPreviewOnly,
|
|
123
|
+
normalizedPath,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const pathSegments = useMemo(() => parsePathSegments(normalizedPath), [normalizedPath]);
|
|
127
|
+
const isCurrentFileLoaded = loadedFilePathRef.current === normalizedPath;
|
|
128
|
+
const renderContent = isCurrentFileLoaded ? content : "";
|
|
129
|
+
const renderInitialContent = isCurrentFileLoaded ? initialContent : "";
|
|
130
|
+
const isDirty = canEditFile && renderContent !== renderInitialContent;
|
|
131
|
+
const isLockedFile =
|
|
132
|
+
canEditFile && matchesBrowsePolicyPath(kLockedBrowsePaths, normalizedPolicyPath);
|
|
133
|
+
const isProtectedFile =
|
|
134
|
+
canEditFile &&
|
|
135
|
+
!isLockedFile &&
|
|
136
|
+
matchesBrowsePolicyPath(kProtectedBrowsePaths, normalizedPolicyPath);
|
|
137
|
+
const isProtectedLocked = isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
|
|
138
|
+
const isEditBlocked = isLockedFile || isProtectedLocked;
|
|
139
|
+
const isDeleteBlocked = isLockedFile || isProtectedFile;
|
|
140
|
+
const canDeleteFile =
|
|
141
|
+
hasSelectedPath &&
|
|
142
|
+
!isFolderPath &&
|
|
143
|
+
!isPreviewOnly &&
|
|
144
|
+
!isDiffView &&
|
|
145
|
+
!deleting &&
|
|
146
|
+
!saving &&
|
|
147
|
+
!isDeleteBlocked;
|
|
148
|
+
const syntaxKind = useMemo(() => getFileSyntaxKind(normalizedPath), [normalizedPath]);
|
|
149
|
+
const isMarkdownFile = syntaxKind === "markdown";
|
|
150
|
+
const shouldUseHighlightedEditor = syntaxKind !== "plain";
|
|
151
|
+
const parsedFrontmatter = useMemo(
|
|
152
|
+
() => (isMarkdownFile ? parseFrontmatter(renderContent) : { entries: [], body: renderContent }),
|
|
153
|
+
[renderContent, isMarkdownFile],
|
|
154
|
+
);
|
|
155
|
+
const highlightedEditorLines = useMemo(
|
|
156
|
+
() => (shouldUseHighlightedEditor ? highlightEditorLines(renderContent, syntaxKind) : []),
|
|
157
|
+
[renderContent, shouldUseHighlightedEditor, syntaxKind],
|
|
158
|
+
);
|
|
159
|
+
const editorLineNumbers = useMemo(() => {
|
|
160
|
+
const lineCount = String(renderContent || "").split("\n").length;
|
|
161
|
+
return Array.from({ length: lineCount }, (_, index) => index + 1);
|
|
162
|
+
}, [renderContent]);
|
|
163
|
+
const previewHtml = useMemo(
|
|
164
|
+
() =>
|
|
165
|
+
isMarkdownFile
|
|
166
|
+
? marked.parse(parsedFrontmatter.body || "", {
|
|
167
|
+
gfm: true,
|
|
168
|
+
breaks: true,
|
|
169
|
+
})
|
|
170
|
+
: "",
|
|
171
|
+
[parsedFrontmatter.body, isMarkdownFile],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const syncEditorLineNumberHeights = useCallback(() => {
|
|
175
|
+
if (!shouldUseHighlightedEditor || viewMode !== "edit") return;
|
|
176
|
+
const numberRows = editorLineNumberRowRefs.current;
|
|
177
|
+
const highlightRows = editorHighlightLineRefs.current;
|
|
178
|
+
const rowCount = Math.min(numberRows.length, highlightRows.length);
|
|
179
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
180
|
+
const numberRow = numberRows[index];
|
|
181
|
+
const highlightRow = highlightRows[index];
|
|
182
|
+
if (!numberRow || !highlightRow) continue;
|
|
183
|
+
numberRow.style.height = `${highlightRow.offsetHeight}px`;
|
|
184
|
+
}
|
|
185
|
+
}, [shouldUseHighlightedEditor, viewMode]);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
syncEditorLineNumberHeights();
|
|
189
|
+
}, [content, syncEditorLineNumberHeights]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!shouldUseHighlightedEditor || viewMode !== "edit") return () => {};
|
|
193
|
+
const onResize = () => syncEditorLineNumberHeights();
|
|
194
|
+
window.addEventListener("resize", onResize);
|
|
195
|
+
return () => window.removeEventListener("resize", onResize);
|
|
196
|
+
}, [shouldUseHighlightedEditor, viewMode, syncEditorLineNumberHeights]);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!isMarkdownFile && viewMode !== "edit") {
|
|
200
|
+
setViewMode("edit");
|
|
201
|
+
}
|
|
202
|
+
}, [isMarkdownFile, viewMode]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setProtectedEditBypassPaths(new Set());
|
|
206
|
+
}, [normalizedPath]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
try {
|
|
210
|
+
window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
|
|
211
|
+
} catch {}
|
|
212
|
+
}, [viewMode]);
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!loading) {
|
|
216
|
+
setShowDelayedLoadingSpinner(false);
|
|
217
|
+
return () => {};
|
|
218
|
+
}
|
|
219
|
+
const timer = window.setTimeout(() => {
|
|
220
|
+
setShowDelayedLoadingSpinner(true);
|
|
221
|
+
}, kLoadingIndicatorDelayMs);
|
|
222
|
+
return () => window.clearTimeout(timer);
|
|
223
|
+
}, [loading]);
|
|
224
|
+
|
|
225
|
+
useFileViewerDraftSync({
|
|
226
|
+
loadedFilePathRef,
|
|
227
|
+
normalizedPath,
|
|
228
|
+
canEditFile,
|
|
229
|
+
hasSelectedPath,
|
|
230
|
+
loading,
|
|
231
|
+
content,
|
|
232
|
+
initialContent,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
useEditorSelectionRestore({
|
|
236
|
+
canEditFile,
|
|
237
|
+
isEditBlocked,
|
|
238
|
+
loading,
|
|
239
|
+
hasSelectedPath,
|
|
240
|
+
normalizedPath,
|
|
241
|
+
loadedFilePathRef,
|
|
242
|
+
restoredSelectionPathRef,
|
|
243
|
+
viewMode,
|
|
244
|
+
content,
|
|
245
|
+
editorTextareaRef,
|
|
246
|
+
editorLineNumbersRef,
|
|
247
|
+
editorHighlightRef,
|
|
248
|
+
viewScrollRatioRef,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const handleSave = useCallback(async () => {
|
|
252
|
+
if (!canEditFile || saving || !isDirty || isEditBlocked) return;
|
|
253
|
+
setSaving(true);
|
|
254
|
+
setError("");
|
|
255
|
+
try {
|
|
256
|
+
await saveFileContent(normalizedPath, content);
|
|
257
|
+
setInitialContent(content);
|
|
258
|
+
setExternalChangeNoticeShown(false);
|
|
259
|
+
clearStoredFileDraft(normalizedPath);
|
|
260
|
+
updateDraftIndex(normalizedPath, false, {
|
|
261
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
262
|
+
});
|
|
263
|
+
window.dispatchEvent(
|
|
264
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
265
|
+
detail: { path: normalizedPath },
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
showToast("Saved", "success");
|
|
269
|
+
} catch (saveError) {
|
|
270
|
+
const message = saveError.message || "Could not save file";
|
|
271
|
+
setError(message);
|
|
272
|
+
showToast(message, "error");
|
|
273
|
+
} finally {
|
|
274
|
+
setSaving(false);
|
|
275
|
+
}
|
|
276
|
+
}, [canEditFile, saving, isDirty, isEditBlocked, normalizedPath, content]);
|
|
277
|
+
|
|
278
|
+
const handleDelete = useCallback(async () => {
|
|
279
|
+
if (!canDeleteFile) return;
|
|
280
|
+
setDeleting(true);
|
|
281
|
+
setError("");
|
|
282
|
+
try {
|
|
283
|
+
const data = await deleteBrowseFile(normalizedPath);
|
|
284
|
+
const deletedPath = String(data?.path || normalizedPath);
|
|
285
|
+
setExternalChangeNoticeShown(false);
|
|
286
|
+
clearStoredFileDraft(normalizedPath);
|
|
287
|
+
updateDraftIndex(normalizedPath, false, {
|
|
288
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
289
|
+
});
|
|
290
|
+
window.dispatchEvent(
|
|
291
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
292
|
+
detail: { path: deletedPath },
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
window.dispatchEvent(
|
|
296
|
+
new CustomEvent("alphaclaw:browse-file-deleted", {
|
|
297
|
+
detail: { path: deletedPath },
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
|
|
301
|
+
showToast("File deleted", "success");
|
|
302
|
+
onRequestClearSelection();
|
|
303
|
+
} catch (deleteError) {
|
|
304
|
+
const message = deleteError.message || "Could not delete file";
|
|
305
|
+
setError(message);
|
|
306
|
+
if (/path is not a file/i.test(message)) {
|
|
307
|
+
showToast("Only files can be deleted", "warning");
|
|
308
|
+
onRequestClearSelection();
|
|
309
|
+
} else {
|
|
310
|
+
showToast(message, "error");
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
setDeleting(false);
|
|
314
|
+
}
|
|
315
|
+
}, [canDeleteFile, normalizedPath, onRequestClearSelection]);
|
|
316
|
+
|
|
317
|
+
const handleRestore = useCallback(async () => {
|
|
318
|
+
if (!isDiffView || !diffStatus?.isDeleted || restoring) return;
|
|
319
|
+
setRestoring(true);
|
|
320
|
+
try {
|
|
321
|
+
const data = await restoreBrowseFile(normalizedPath);
|
|
322
|
+
const restoredPath = String(data?.path || normalizedPath);
|
|
323
|
+
window.dispatchEvent(
|
|
324
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
325
|
+
detail: { path: restoredPath },
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
|
|
329
|
+
showToast("File restored", "success");
|
|
330
|
+
onRequestEdit(restoredPath);
|
|
331
|
+
} catch (restoreError) {
|
|
332
|
+
showToast(restoreError.message || "Could not restore file", "error");
|
|
333
|
+
} finally {
|
|
334
|
+
setRestoring(false);
|
|
335
|
+
}
|
|
336
|
+
}, [
|
|
337
|
+
diffStatus?.isDeleted,
|
|
338
|
+
isDiffView,
|
|
339
|
+
normalizedPath,
|
|
340
|
+
onRequestEdit,
|
|
341
|
+
restoring,
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
useFileViewerHotkeys({
|
|
345
|
+
canEditFile,
|
|
346
|
+
isPreviewOnly,
|
|
347
|
+
isDiffView,
|
|
348
|
+
viewMode,
|
|
349
|
+
handleSave,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const handleEditProtectedFile = () => {
|
|
353
|
+
if (!normalizedPolicyPath) return;
|
|
354
|
+
setProtectedEditBypassPaths((previousPaths) => {
|
|
355
|
+
const nextPaths = new Set(previousPaths);
|
|
356
|
+
nextPaths.add(normalizedPolicyPath);
|
|
357
|
+
return nextPaths;
|
|
358
|
+
});
|
|
359
|
+
window.requestAnimationFrame(() => {
|
|
360
|
+
window.requestAnimationFrame(() => {
|
|
361
|
+
const textareaElement = editorTextareaRef.current;
|
|
362
|
+
if (!textareaElement) return;
|
|
363
|
+
if (textareaElement.disabled || textareaElement.readOnly) return;
|
|
364
|
+
textareaElement.focus();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const handleContentInput = (event) => {
|
|
370
|
+
if (isEditBlocked || isPreviewOnly) return;
|
|
371
|
+
const nextContent = event.target.value;
|
|
372
|
+
setContent(nextContent);
|
|
373
|
+
if (hasSelectedPath && canEditFile) {
|
|
374
|
+
writeStoredEditorSelection(normalizedPath, {
|
|
375
|
+
start: event.target.selectionStart,
|
|
376
|
+
end: event.target.selectionEnd,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (hasSelectedPath && canEditFile) {
|
|
380
|
+
writeStoredFileDraft(normalizedPath, nextContent);
|
|
381
|
+
updateDraftIndex(normalizedPath, nextContent !== initialContent, {
|
|
382
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const handleEditorSelectionChange = () => {
|
|
388
|
+
if (!hasSelectedPath || !canEditFile || loading) return;
|
|
389
|
+
const textareaElement = editorTextareaRef.current;
|
|
390
|
+
if (!textareaElement) return;
|
|
391
|
+
writeStoredEditorSelection(normalizedPath, {
|
|
392
|
+
start: textareaElement.selectionStart,
|
|
393
|
+
end: textareaElement.selectionEnd,
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
state: {
|
|
399
|
+
hasSelectedPath,
|
|
400
|
+
isPreviewOnly,
|
|
401
|
+
loading,
|
|
402
|
+
saving,
|
|
403
|
+
deleting,
|
|
404
|
+
restoring,
|
|
405
|
+
showDelayedLoadingSpinner,
|
|
406
|
+
error,
|
|
407
|
+
isFolderPath,
|
|
408
|
+
isImageFile,
|
|
409
|
+
imageDataUrl,
|
|
410
|
+
isAudioFile,
|
|
411
|
+
audioDataUrl,
|
|
412
|
+
isSqliteFile,
|
|
413
|
+
sqliteSummary,
|
|
414
|
+
sqliteSelectedTable,
|
|
415
|
+
sqliteTableOffset,
|
|
416
|
+
sqliteTableLoading,
|
|
417
|
+
sqliteTableError,
|
|
418
|
+
sqliteTableData,
|
|
419
|
+
isDiffView,
|
|
420
|
+
diffLoading,
|
|
421
|
+
diffError,
|
|
422
|
+
diffContent,
|
|
423
|
+
diffStatus,
|
|
424
|
+
isMarkdownFile,
|
|
425
|
+
frontmatterCollapsed,
|
|
426
|
+
previewHtml,
|
|
427
|
+
viewMode,
|
|
428
|
+
renderContent,
|
|
429
|
+
},
|
|
430
|
+
derived: {
|
|
431
|
+
pathSegments,
|
|
432
|
+
isDirty,
|
|
433
|
+
canEditFile,
|
|
434
|
+
canDeleteFile,
|
|
435
|
+
isDeleteBlocked,
|
|
436
|
+
isEditBlocked,
|
|
437
|
+
isLockedFile,
|
|
438
|
+
isProtectedFile,
|
|
439
|
+
isProtectedLocked,
|
|
440
|
+
shouldUseHighlightedEditor,
|
|
441
|
+
parsedFrontmatter,
|
|
442
|
+
highlightedEditorLines,
|
|
443
|
+
editorLineNumbers,
|
|
444
|
+
},
|
|
445
|
+
refs: {
|
|
446
|
+
previewRef,
|
|
447
|
+
editorLineNumbersRef,
|
|
448
|
+
editorLineNumberRowRefs,
|
|
449
|
+
editorHighlightRef,
|
|
450
|
+
editorHighlightLineRefs,
|
|
451
|
+
editorTextareaRef,
|
|
452
|
+
},
|
|
453
|
+
actions: {
|
|
454
|
+
setFrontmatterCollapsed,
|
|
455
|
+
setSqliteSelectedTable,
|
|
456
|
+
setSqliteTableOffset,
|
|
457
|
+
handleChangeViewMode,
|
|
458
|
+
handleSave,
|
|
459
|
+
handleDelete,
|
|
460
|
+
handleRestore,
|
|
461
|
+
handleEditProtectedFile,
|
|
462
|
+
handleContentInput,
|
|
463
|
+
handleEditorScroll,
|
|
464
|
+
handlePreviewScroll,
|
|
465
|
+
handleEditorSelectionChange,
|
|
466
|
+
},
|
|
467
|
+
context: {
|
|
468
|
+
normalizedPath,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
};
|
|
@@ -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
|
+
};
|