@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.
Files changed (64) hide show
  1. package/bin/alphaclaw.js +66 -32
  2. package/lib/public/assets/icons/google_icon.svg +8 -0
  3. package/lib/public/css/explorer.css +254 -6
  4. package/lib/public/js/app.js +165 -100
  5. package/lib/public/js/components/channels.js +1 -0
  6. package/lib/public/js/components/credentials-modal.js +36 -8
  7. package/lib/public/js/components/file-tree.js +267 -88
  8. package/lib/public/js/components/file-viewer/constants.js +6 -0
  9. package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
  10. package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
  11. package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
  12. package/lib/public/js/components/file-viewer/index.js +202 -0
  13. package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
  14. package/lib/public/js/components/file-viewer/media-preview.js +44 -0
  15. package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
  16. package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
  17. package/lib/public/js/components/file-viewer/status-banners.js +64 -0
  18. package/lib/public/js/components/file-viewer/storage.js +58 -0
  19. package/lib/public/js/components/file-viewer/toolbar.js +119 -0
  20. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +93 -0
  21. package/lib/public/js/components/file-viewer/use-file-diff.js +60 -0
  22. package/lib/public/js/components/file-viewer/use-file-loader.js +312 -0
  23. package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
  24. package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
  25. package/lib/public/js/components/file-viewer/use-file-viewer.js +471 -0
  26. package/lib/public/js/components/file-viewer/utils.js +11 -0
  27. package/lib/public/js/components/gateway.js +83 -30
  28. package/lib/public/js/components/google/account-row.js +98 -0
  29. package/lib/public/js/components/google/add-account-modal.js +93 -0
  30. package/lib/public/js/components/google/index.js +439 -0
  31. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  32. package/lib/public/js/components/icons.js +39 -0
  33. package/lib/public/js/components/sidebar-git-panel.js +115 -25
  34. package/lib/public/js/components/sidebar.js +91 -75
  35. package/lib/public/js/components/usage-tab.js +4 -1
  36. package/lib/public/js/components/watchdog-tab.js +6 -0
  37. package/lib/public/js/lib/api.js +88 -8
  38. package/lib/public/js/lib/browse-file-policies.js +52 -0
  39. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  40. package/lib/public/shared/browse-file-policies.json +13 -0
  41. package/lib/scripts/git +40 -0
  42. package/lib/scripts/git-askpass +6 -0
  43. package/lib/server/constants.js +20 -0
  44. package/lib/server/google-state.js +187 -0
  45. package/lib/server/helpers.js +12 -4
  46. package/lib/server/onboarding/github.js +21 -2
  47. package/lib/server/onboarding/index.js +1 -3
  48. package/lib/server/onboarding/openclaw.js +3 -0
  49. package/lib/server/onboarding/workspace.js +40 -0
  50. package/lib/server/routes/browse/constants.js +51 -0
  51. package/lib/server/routes/browse/file-helpers.js +43 -0
  52. package/lib/server/routes/browse/git.js +131 -0
  53. package/lib/server/routes/browse/index.js +660 -0
  54. package/lib/server/routes/browse/path-utils.js +53 -0
  55. package/lib/server/routes/browse/sqlite.js +140 -0
  56. package/lib/server/routes/google.js +414 -213
  57. package/lib/server/routes/proxy.js +11 -5
  58. package/lib/setup/core-prompts/TOOLS.md +0 -4
  59. package/lib/setup/gitignore +3 -0
  60. package/lib/setup/hourly-git-sync.sh +28 -1
  61. package/package.json +1 -1
  62. package/lib/public/js/components/file-viewer.js +0 -1095
  63. package/lib/public/js/components/google.js +0 -228
  64. package/lib/server/routes/browse.js +0 -500
@@ -0,0 +1,312 @@
1
+ import { useEffect, useRef } from "https://esm.sh/preact/hooks";
2
+ import { fetchBrowseSqliteTable, fetchFileContent } from "../../lib/api.js";
3
+ import {
4
+ clearStoredFileDraft,
5
+ readStoredFileDraft,
6
+ updateDraftIndex,
7
+ } from "../../lib/browse-draft-state.js";
8
+ import { showToast } from "../toast.js";
9
+ import { kFileRefreshIntervalMs, kSqlitePageSize } from "./constants.js";
10
+
11
+ export const useFileLoader = ({
12
+ hasSelectedPath,
13
+ normalizedPath,
14
+ isDiffView,
15
+ isSqliteFile,
16
+ sqliteSelectedTable,
17
+ sqliteTableOffset,
18
+ canEditFile,
19
+ isFolderPath,
20
+ loading,
21
+ saving,
22
+ initialContent,
23
+ isDirty,
24
+ setContent,
25
+ setInitialContent,
26
+ setFileKind,
27
+ setImageDataUrl,
28
+ setAudioDataUrl,
29
+ setSqliteSummary,
30
+ setSqliteSelectedTable,
31
+ setSqliteTableOffset,
32
+ setSqliteTableLoading,
33
+ setSqliteTableError,
34
+ setSqliteTableData,
35
+ setError,
36
+ setIsFolderPath,
37
+ setExternalChangeNoticeShown,
38
+ externalChangeNoticeShown,
39
+ viewScrollRatioRef,
40
+ setLoading,
41
+ }) => {
42
+ const loadedFilePathRef = useRef("");
43
+ const restoredSelectionPathRef = useRef("");
44
+ const fileRefreshInFlightRef = useRef(false);
45
+
46
+ useEffect(() => {
47
+ let active = true;
48
+ loadedFilePathRef.current = "";
49
+ restoredSelectionPathRef.current = "";
50
+ if (!hasSelectedPath) {
51
+ setContent("");
52
+ setInitialContent("");
53
+ setFileKind("text");
54
+ setImageDataUrl("");
55
+ setAudioDataUrl("");
56
+ setSqliteSummary(null);
57
+ setSqliteSelectedTable("");
58
+ setSqliteTableOffset(0);
59
+ setSqliteTableLoading(false);
60
+ setSqliteTableError("");
61
+ setSqliteTableData(null);
62
+ setError("");
63
+ setIsFolderPath(false);
64
+ viewScrollRatioRef.current = 0;
65
+ loadedFilePathRef.current = "";
66
+ return () => {
67
+ active = false;
68
+ };
69
+ }
70
+ // Clear previous file state immediately so large content from the last
71
+ // file is never rendered/parses under the next file's syntax mode.
72
+ setContent("");
73
+ setInitialContent("");
74
+ setImageDataUrl("");
75
+ setAudioDataUrl("");
76
+ setSqliteSummary(null);
77
+ setSqliteSelectedTable("");
78
+ setSqliteTableOffset(0);
79
+ setSqliteTableLoading(false);
80
+ setSqliteTableError("");
81
+ setSqliteTableData(null);
82
+ setFileKind("text");
83
+ setError("");
84
+ setIsFolderPath(false);
85
+ setExternalChangeNoticeShown(false);
86
+ viewScrollRatioRef.current = 0;
87
+ if (isDiffView) {
88
+ setLoading(false);
89
+ loadedFilePathRef.current = normalizedPath;
90
+ restoredSelectionPathRef.current = "";
91
+ return () => {
92
+ active = false;
93
+ };
94
+ }
95
+
96
+ const loadFile = async () => {
97
+ setLoading(true);
98
+ setError("");
99
+ setIsFolderPath(false);
100
+ try {
101
+ const data = await fetchFileContent(normalizedPath);
102
+ if (!active) return;
103
+ const nextFileKind =
104
+ data?.kind === "image"
105
+ ? "image"
106
+ : data?.kind === "audio"
107
+ ? "audio"
108
+ : data?.kind === "sqlite"
109
+ ? "sqlite"
110
+ : "text";
111
+ setFileKind(nextFileKind);
112
+ if (nextFileKind === "image") {
113
+ setImageDataUrl(String(data?.imageDataUrl || ""));
114
+ setAudioDataUrl("");
115
+ setSqliteSummary(null);
116
+ setSqliteSelectedTable("");
117
+ setSqliteTableOffset(0);
118
+ setSqliteTableLoading(false);
119
+ setSqliteTableError("");
120
+ setSqliteTableData(null);
121
+ setContent("");
122
+ setInitialContent("");
123
+ setExternalChangeNoticeShown(false);
124
+ viewScrollRatioRef.current = 0;
125
+ loadedFilePathRef.current = normalizedPath;
126
+ restoredSelectionPathRef.current = "";
127
+ return;
128
+ }
129
+ if (nextFileKind === "audio") {
130
+ setAudioDataUrl(String(data?.audioDataUrl || ""));
131
+ setImageDataUrl("");
132
+ setSqliteSummary(null);
133
+ setSqliteSelectedTable("");
134
+ setSqliteTableOffset(0);
135
+ setSqliteTableLoading(false);
136
+ setSqliteTableError("");
137
+ setSqliteTableData(null);
138
+ setContent("");
139
+ setInitialContent("");
140
+ setExternalChangeNoticeShown(false);
141
+ viewScrollRatioRef.current = 0;
142
+ loadedFilePathRef.current = normalizedPath;
143
+ restoredSelectionPathRef.current = "";
144
+ return;
145
+ }
146
+ if (nextFileKind === "sqlite") {
147
+ const nextSqliteSummary = data?.sqliteSummary || null;
148
+ const nextObjects = nextSqliteSummary?.objects || [];
149
+ const defaultTable =
150
+ nextObjects.find((entry) => entry?.type === "table")?.name ||
151
+ nextObjects[0]?.name ||
152
+ "";
153
+ setSqliteSummary(nextSqliteSummary);
154
+ setSqliteSelectedTable(defaultTable);
155
+ setSqliteTableOffset(0);
156
+ setSqliteTableLoading(false);
157
+ setSqliteTableError("");
158
+ setSqliteTableData(null);
159
+ setImageDataUrl("");
160
+ setAudioDataUrl("");
161
+ setContent("");
162
+ setInitialContent("");
163
+ setExternalChangeNoticeShown(false);
164
+ viewScrollRatioRef.current = 0;
165
+ loadedFilePathRef.current = normalizedPath;
166
+ restoredSelectionPathRef.current = "";
167
+ return;
168
+ }
169
+ setImageDataUrl("");
170
+ setAudioDataUrl("");
171
+ setSqliteSummary(null);
172
+ setSqliteSelectedTable("");
173
+ setSqliteTableOffset(0);
174
+ setSqliteTableLoading(false);
175
+ setSqliteTableError("");
176
+ setSqliteTableData(null);
177
+ const nextContent = data.content || "";
178
+ const draftContent = readStoredFileDraft(normalizedPath);
179
+ setContent(draftContent || nextContent);
180
+ updateDraftIndex(normalizedPath, Boolean(draftContent && draftContent !== nextContent), {
181
+ dispatchEvent: (event) => window.dispatchEvent(event),
182
+ });
183
+ setInitialContent(nextContent);
184
+ setExternalChangeNoticeShown(false);
185
+ viewScrollRatioRef.current = 0;
186
+ loadedFilePathRef.current = normalizedPath;
187
+ restoredSelectionPathRef.current = "";
188
+ } catch (loadError) {
189
+ if (!active) return;
190
+ setFileKind("text");
191
+ setImageDataUrl("");
192
+ setAudioDataUrl("");
193
+ setSqliteSummary(null);
194
+ setSqliteSelectedTable("");
195
+ setSqliteTableOffset(0);
196
+ setSqliteTableLoading(false);
197
+ setSqliteTableError("");
198
+ setSqliteTableData(null);
199
+ const message = loadError.message || "Could not load file";
200
+ if (/path is not a file/i.test(message)) {
201
+ setContent("");
202
+ setInitialContent("");
203
+ setIsFolderPath(true);
204
+ setError("");
205
+ loadedFilePathRef.current = normalizedPath;
206
+ restoredSelectionPathRef.current = "";
207
+ return;
208
+ }
209
+ setError(message);
210
+ } finally {
211
+ if (active) setLoading(false);
212
+ }
213
+ };
214
+ loadFile();
215
+ return () => {
216
+ active = false;
217
+ };
218
+ }, [hasSelectedPath, normalizedPath, isDiffView]);
219
+
220
+ useEffect(() => {
221
+ if (!isSqliteFile || !normalizedPath || !sqliteSelectedTable) {
222
+ setSqliteTableData(null);
223
+ setSqliteTableError("");
224
+ setSqliteTableLoading(false);
225
+ return () => {};
226
+ }
227
+ let active = true;
228
+ const loadSqliteTable = async () => {
229
+ setSqliteTableLoading(true);
230
+ setSqliteTableError("");
231
+ try {
232
+ const tableData = await fetchBrowseSqliteTable({
233
+ filePath: normalizedPath,
234
+ table: sqliteSelectedTable,
235
+ limit: kSqlitePageSize,
236
+ offset: sqliteTableOffset,
237
+ });
238
+ if (!active) return;
239
+ setSqliteTableData(tableData);
240
+ } catch (nextError) {
241
+ if (!active) return;
242
+ setSqliteTableError(nextError.message || "Could not load sqlite table");
243
+ } finally {
244
+ if (active) setSqliteTableLoading(false);
245
+ }
246
+ };
247
+ loadSqliteTable();
248
+ return () => {
249
+ active = false;
250
+ };
251
+ }, [isSqliteFile, normalizedPath, sqliteSelectedTable, sqliteTableOffset]);
252
+
253
+ useEffect(() => {
254
+ if (!hasSelectedPath || isFolderPath || !canEditFile || isDiffView) return () => {};
255
+ const refreshFromDisk = async () => {
256
+ if (loading || saving) return;
257
+ if (fileRefreshInFlightRef.current) return;
258
+ fileRefreshInFlightRef.current = true;
259
+ try {
260
+ const data = await fetchFileContent(normalizedPath);
261
+ const diskContent = data.content || "";
262
+ if (diskContent === initialContent) {
263
+ setExternalChangeNoticeShown(false);
264
+ return;
265
+ }
266
+ // Auto-refresh only when editor has no unsaved work.
267
+ if (!isDirty) {
268
+ setContent(diskContent);
269
+ setInitialContent(diskContent);
270
+ clearStoredFileDraft(normalizedPath);
271
+ updateDraftIndex(normalizedPath, false, {
272
+ dispatchEvent: (event) => window.dispatchEvent(event),
273
+ });
274
+ setExternalChangeNoticeShown(false);
275
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
276
+ return;
277
+ }
278
+ if (!externalChangeNoticeShown) {
279
+ showToast(
280
+ "This file changed on disk. Save to overwrite or reload by re-opening.",
281
+ "error",
282
+ );
283
+ setExternalChangeNoticeShown(true);
284
+ }
285
+ } catch {
286
+ // Ignore transient refresh errors to avoid interrupting editing.
287
+ } finally {
288
+ fileRefreshInFlightRef.current = false;
289
+ }
290
+ };
291
+ const intervalId = window.setInterval(refreshFromDisk, kFileRefreshIntervalMs);
292
+ return () => {
293
+ window.clearInterval(intervalId);
294
+ };
295
+ }, [
296
+ hasSelectedPath,
297
+ isFolderPath,
298
+ canEditFile,
299
+ isDiffView,
300
+ loading,
301
+ saving,
302
+ normalizedPath,
303
+ initialContent,
304
+ isDirty,
305
+ externalChangeNoticeShown,
306
+ ]);
307
+
308
+ return {
309
+ loadedFilePathRef,
310
+ restoredSelectionPathRef,
311
+ };
312
+ };
@@ -0,0 +1,32 @@
1
+ import { useEffect } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ clearStoredFileDraft,
4
+ updateDraftIndex,
5
+ writeStoredFileDraft,
6
+ } from "../../lib/browse-draft-state.js";
7
+
8
+ export const useFileViewerDraftSync = ({
9
+ loadedFilePathRef,
10
+ normalizedPath,
11
+ canEditFile,
12
+ hasSelectedPath,
13
+ loading,
14
+ content,
15
+ initialContent,
16
+ }) => {
17
+ useEffect(() => {
18
+ if (loadedFilePathRef.current !== normalizedPath) return;
19
+ if (!canEditFile || !hasSelectedPath || loading) return;
20
+ if (content === initialContent) {
21
+ clearStoredFileDraft(normalizedPath);
22
+ updateDraftIndex(normalizedPath, false, {
23
+ dispatchEvent: (event) => window.dispatchEvent(event),
24
+ });
25
+ return;
26
+ }
27
+ writeStoredFileDraft(normalizedPath, content);
28
+ updateDraftIndex(normalizedPath, true, {
29
+ dispatchEvent: (event) => window.dispatchEvent(event),
30
+ });
31
+ }, [canEditFile, hasSelectedPath, loading, content, initialContent, normalizedPath]);
32
+ };
@@ -0,0 +1,25 @@
1
+ import { useEffect } from "https://esm.sh/preact/hooks";
2
+
3
+ export const useFileViewerHotkeys = ({
4
+ canEditFile,
5
+ isPreviewOnly,
6
+ isDiffView,
7
+ viewMode,
8
+ handleSave,
9
+ }) => {
10
+ useEffect(() => {
11
+ const handleKeyDown = (event) => {
12
+ const isSaveShortcut =
13
+ (event.metaKey || event.ctrlKey) &&
14
+ !event.shiftKey &&
15
+ !event.altKey &&
16
+ String(event.key || "").toLowerCase() === "s";
17
+ if (!isSaveShortcut) return;
18
+ if (!canEditFile || isPreviewOnly || isDiffView || viewMode !== "edit") return;
19
+ event.preventDefault();
20
+ void handleSave();
21
+ };
22
+ window.addEventListener("keydown", handleKeyDown);
23
+ return () => window.removeEventListener("keydown", handleKeyDown);
24
+ }, [canEditFile, isPreviewOnly, isDiffView, viewMode, handleSave]);
25
+ };