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