@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-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.
Files changed (68) hide show
  1. package/bin/alphaclaw.js +1 -31
  2. package/lib/public/assets/icons/google_icon.svg +8 -0
  3. package/lib/public/css/explorer.css +53 -0
  4. package/lib/public/css/shell.css +21 -19
  5. package/lib/public/css/theme.css +17 -0
  6. package/lib/public/js/app.js +205 -109
  7. package/lib/public/js/components/credentials-modal.js +36 -8
  8. package/lib/public/js/components/file-tree.js +212 -22
  9. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  10. package/lib/public/js/components/file-viewer/index.js +47 -6
  11. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  12. package/lib/public/js/components/file-viewer/status-banners.js +11 -6
  13. package/lib/public/js/components/file-viewer/toolbar.js +56 -1
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
  15. package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
  16. package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
  17. package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
  18. package/lib/public/js/components/google/account-row.js +131 -0
  19. package/lib/public/js/components/google/add-account-modal.js +93 -0
  20. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  21. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  22. package/lib/public/js/components/google/index.js +553 -0
  23. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  24. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  25. package/lib/public/js/components/icons.js +26 -0
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/sidebar-git-panel.js +48 -20
  28. package/lib/public/js/components/sidebar.js +93 -75
  29. package/lib/public/js/components/toast.js +11 -7
  30. package/lib/public/js/components/usage-tab/constants.js +31 -0
  31. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  32. package/lib/public/js/components/usage-tab/index.js +72 -0
  33. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  34. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  35. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  36. package/lib/public/js/components/webhooks.js +182 -129
  37. package/lib/public/js/lib/api.js +178 -9
  38. package/lib/public/js/lib/browse-file-policies.js +29 -11
  39. package/lib/public/js/lib/format.js +71 -0
  40. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  41. package/lib/public/shared/browse-file-policies.json +13 -0
  42. package/lib/server/constants.js +47 -7
  43. package/lib/server/gmail-push.js +109 -0
  44. package/lib/server/gmail-serve.js +254 -0
  45. package/lib/server/gmail-watch.js +725 -0
  46. package/lib/server/google-state.js +317 -0
  47. package/lib/server/helpers.js +17 -11
  48. package/lib/server/internal-files-migration.js +31 -3
  49. package/lib/server/onboarding/github.js +21 -2
  50. package/lib/server/onboarding/index.js +1 -3
  51. package/lib/server/onboarding/openclaw.js +3 -0
  52. package/lib/server/onboarding/workspace.js +40 -0
  53. package/lib/server/routes/browse/index.js +90 -2
  54. package/lib/server/routes/gmail.js +128 -0
  55. package/lib/server/routes/google.js +433 -213
  56. package/lib/server/routes/system.js +107 -0
  57. package/lib/server/routes/usage.js +29 -2
  58. package/lib/server/routes/webhooks.js +52 -17
  59. package/lib/server/usage-db.js +283 -15
  60. package/lib/server/watchdog.js +66 -0
  61. package/lib/server/webhook-middleware.js +99 -1
  62. package/lib/server/webhooks.js +214 -65
  63. package/lib/server.js +27 -0
  64. package/lib/setup/gitignore +6 -0
  65. package/lib/setup/hourly-git-sync.sh +29 -2
  66. package/package.json +1 -1
  67. package/lib/public/js/components/google.js +0 -228
  68. package/lib/public/js/components/usage-tab.js +0 -531
@@ -18,6 +18,7 @@ export const MarkdownSplitView = ({
18
18
  editorTextareaRef,
19
19
  renderContent,
20
20
  handleContentInput,
21
+ handleEditorKeyDown,
21
22
  handleEditorScroll,
22
23
  handleEditorSelectionChange,
23
24
  isEditBlocked,
@@ -43,6 +44,7 @@ export const MarkdownSplitView = ({
43
44
  editorTextareaRef=${editorTextareaRef}
44
45
  renderContent=${renderContent}
45
46
  handleContentInput=${handleContentInput}
47
+ handleEditorKeyDown=${handleEditorKeyDown}
46
48
  handleEditorScroll=${handleEditorScroll}
47
49
  handleEditorSelectionChange=${handleEditorSelectionChange}
48
50
  isEditBlocked=${isEditBlocked}
@@ -9,6 +9,7 @@ export const FileViewerStatusBanners = ({
9
9
  isDiffView,
10
10
  onRequestEdit,
11
11
  normalizedPath,
12
+ isDeletedDiff = false,
12
13
  isLockedFile,
13
14
  isProtectedFile,
14
15
  isProtectedLocked,
@@ -18,12 +19,16 @@ export const FileViewerStatusBanners = ({
18
19
  ? html`
19
20
  <div class="file-viewer-protected-banner file-viewer-diff-banner">
20
21
  <div class="file-viewer-protected-banner-text">Viewing unsynced changes</div>
21
- <${ActionButton}
22
- onClick=${() => onRequestEdit(normalizedPath)}
23
- tone="secondary"
24
- size="sm"
25
- idleLabel="View file"
26
- />
22
+ ${!isDeletedDiff
23
+ ? html`
24
+ <${ActionButton}
25
+ onClick=${() => onRequestEdit(normalizedPath)}
26
+ tone="secondary"
27
+ size="sm"
28
+ idleLabel="View file"
29
+ />
30
+ `
31
+ : null}
27
32
  </div>
28
33
  `
29
34
  : null}
@@ -2,7 +2,7 @@ import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
3
  import { ActionButton } from "../action-button.js";
4
4
  import { SegmentedControl } from "../segmented-control.js";
5
- import { SaveFillIcon } from "../icons.js";
5
+ import { DeleteBinLineIcon, RestartLineIcon, SaveFillIcon } from "../icons.js";
6
6
 
7
7
  const html = htm.bind(h);
8
8
 
@@ -15,6 +15,7 @@ export const FileViewerToolbar = ({
15
15
  viewMode,
16
16
  handleChangeViewMode,
17
17
  handleSave,
18
+ handleDiscard,
18
19
  loading,
19
20
  canEditFile,
20
21
  isEditBlocked,
@@ -22,6 +23,14 @@ export const FileViewerToolbar = ({
22
23
  isAudioFile,
23
24
  isSqliteFile,
24
25
  saving,
26
+ deleting,
27
+ restoring,
28
+ canDeleteFile,
29
+ isDeleteBlocked,
30
+ isProtectedFile,
31
+ canRestoreDeletedDiff,
32
+ onRequestDelete,
33
+ onRequestRestore,
25
34
  }) => html`
26
35
  <div class="file-viewer-tabbar">
27
36
  <div class="file-viewer-tab active">
@@ -58,6 +67,36 @@ export const FileViewerToolbar = ({
58
67
  ${!isDiffView
59
68
  ? !isImageFile && !isAudioFile && !isSqliteFile
60
69
  ? html`
70
+ ${!isProtectedFile
71
+ ? html`
72
+ <button
73
+ type="button"
74
+ onclick=${onRequestDelete}
75
+ disabled=${!canDeleteFile || deleting}
76
+ class=${`file-viewer-icon-action file-viewer-delete-action ${
77
+ !canDeleteFile || deleting ? "is-disabled" : ""
78
+ }`.trim()}
79
+ title=${isDeleteBlocked
80
+ ? "Locked files cannot be deleted"
81
+ : "Delete file"}
82
+ aria-label="Delete file"
83
+ >
84
+ <${DeleteBinLineIcon} className="file-viewer-icon-action-icon" />
85
+ </button>
86
+ `
87
+ : null}
88
+ ${isDirty
89
+ ? html`
90
+ <${ActionButton}
91
+ onClick=${handleDiscard}
92
+ disabled=${loading || !canEditFile || isEditBlocked || deleting || saving}
93
+ tone="secondary"
94
+ size="sm"
95
+ idleLabel="Discard changes"
96
+ className="file-viewer-save-action"
97
+ />
98
+ `
99
+ : null}
61
100
  <${ActionButton}
62
101
  onClick=${handleSave}
63
102
  disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
@@ -73,5 +112,21 @@ export const FileViewerToolbar = ({
73
112
  `
74
113
  : null
75
114
  : null}
115
+ ${isDiffView && canRestoreDeletedDiff
116
+ ? html`
117
+ <${ActionButton}
118
+ onClick=${onRequestRestore}
119
+ disabled=${restoring}
120
+ loading=${restoring}
121
+ tone="secondary"
122
+ size="sm"
123
+ idleLabel="Restore"
124
+ loadingLabel="Restoring..."
125
+ idleIcon=${RestartLineIcon}
126
+ idleIconClassName="file-viewer-save-icon"
127
+ className="file-viewer-save-action"
128
+ />
129
+ `
130
+ : null}
76
131
  </div>
77
132
  `;
@@ -5,6 +5,7 @@ import { getScrollRatio } from "./scroll-sync.js";
5
5
 
6
6
  export const useEditorSelectionRestore = ({
7
7
  canEditFile,
8
+ isEditBlocked,
8
9
  loading,
9
10
  hasSelectedPath,
10
11
  normalizedPath,
@@ -18,6 +19,10 @@ export const useEditorSelectionRestore = ({
18
19
  viewScrollRatioRef,
19
20
  }) => {
20
21
  useEffect(() => {
22
+ if (isEditBlocked) {
23
+ restoredSelectionPathRef.current = "";
24
+ return () => {};
25
+ }
21
26
  if (!canEditFile || loading || !hasSelectedPath) return () => {};
22
27
  if (loadedFilePathRef.current !== normalizedPath) return () => {};
23
28
  if (restoredSelectionPathRef.current === normalizedPath) return () => {};
@@ -72,6 +77,7 @@ export const useEditorSelectionRestore = ({
72
77
  };
73
78
  }, [
74
79
  canEditFile,
80
+ isEditBlocked,
75
81
  loading,
76
82
  hasSelectedPath,
77
83
  normalizedPath,
@@ -10,6 +10,10 @@ export const useFileDiff = ({
10
10
  const [diffLoading, setDiffLoading] = useState(false);
11
11
  const [diffError, setDiffError] = useState("");
12
12
  const [diffContent, setDiffContent] = useState("");
13
+ const [diffStatus, setDiffStatus] = useState({
14
+ statusKind: "",
15
+ isDeleted: false,
16
+ });
13
17
 
14
18
  useEffect(() => {
15
19
  let active = true;
@@ -17,6 +21,7 @@ export const useFileDiff = ({
17
21
  setDiffLoading(false);
18
22
  setDiffError("");
19
23
  setDiffContent("");
24
+ setDiffStatus({ statusKind: "", isDeleted: false });
20
25
  return () => {
21
26
  active = false;
22
27
  };
@@ -28,9 +33,14 @@ export const useFileDiff = ({
28
33
  const data = await fetchBrowseFileDiff(normalizedPath);
29
34
  if (!active) return;
30
35
  setDiffContent(String(data?.content || ""));
36
+ setDiffStatus({
37
+ statusKind: String(data?.statusKind || ""),
38
+ isDeleted: !!data?.isDeleted,
39
+ });
31
40
  } catch (nextError) {
32
41
  if (!active) return;
33
42
  setDiffError(nextError.message || "Could not load diff");
43
+ setDiffStatus({ statusKind: "", isDeleted: false });
34
44
  } finally {
35
45
  if (active) setDiffLoading(false);
36
46
  }
@@ -45,5 +55,6 @@ export const useFileDiff = ({
45
55
  diffLoading,
46
56
  diffError,
47
57
  diffContent,
58
+ diffStatus,
48
59
  };
49
60
  };
@@ -11,6 +11,7 @@ import { kFileRefreshIntervalMs, kSqlitePageSize } from "./constants.js";
11
11
  export const useFileLoader = ({
12
12
  hasSelectedPath,
13
13
  normalizedPath,
14
+ isDiffView,
14
15
  isSqliteFile,
15
16
  sqliteSelectedTable,
16
17
  sqliteTableOffset,
@@ -83,6 +84,14 @@ export const useFileLoader = ({
83
84
  setIsFolderPath(false);
84
85
  setExternalChangeNoticeShown(false);
85
86
  viewScrollRatioRef.current = 0;
87
+ if (isDiffView) {
88
+ setLoading(false);
89
+ loadedFilePathRef.current = normalizedPath;
90
+ restoredSelectionPathRef.current = "";
91
+ return () => {
92
+ active = false;
93
+ };
94
+ }
86
95
 
87
96
  const loadFile = async () => {
88
97
  setLoading(true);
@@ -206,7 +215,7 @@ export const useFileLoader = ({
206
215
  return () => {
207
216
  active = false;
208
217
  };
209
- }, [hasSelectedPath, normalizedPath]);
218
+ }, [hasSelectedPath, normalizedPath, isDiffView]);
210
219
 
211
220
  useEffect(() => {
212
221
  if (!isSqliteFile || !normalizedPath || !sqliteSelectedTable) {
@@ -242,7 +251,7 @@ export const useFileLoader = ({
242
251
  }, [isSqliteFile, normalizedPath, sqliteSelectedTable, sqliteTableOffset]);
243
252
 
244
253
  useEffect(() => {
245
- if (!hasSelectedPath || isFolderPath || !canEditFile) return () => {};
254
+ if (!hasSelectedPath || isFolderPath || !canEditFile || isDiffView) return () => {};
246
255
  const refreshFromDisk = async () => {
247
256
  if (loading || saving) return;
248
257
  if (fileRefreshInFlightRef.current) return;
@@ -287,6 +296,7 @@ export const useFileLoader = ({
287
296
  hasSelectedPath,
288
297
  isFolderPath,
289
298
  canEditFile,
299
+ isDiffView,
290
300
  loading,
291
301
  saving,
292
302
  normalizedPath,
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
2
2
  import { marked } from "https://esm.sh/marked";
3
- import { saveFileContent } from "../../lib/api.js";
3
+ import { deleteBrowseFile, restoreBrowseFile, saveFileContent } from "../../lib/api.js";
4
4
  import {
5
5
  getFileSyntaxKind,
6
6
  highlightEditorLines,
@@ -32,6 +32,8 @@ export const useFileViewer = ({
32
32
  filePath = "",
33
33
  isPreviewOnly = false,
34
34
  browseView = "edit",
35
+ onRequestClearSelection = () => {},
36
+ onRequestEdit = () => {},
35
37
  }) => {
36
38
  const normalizedPath = String(filePath || "").trim();
37
39
  const normalizedPolicyPath = normalizeBrowsePolicyPath(normalizedPath);
@@ -50,6 +52,8 @@ export const useFileViewer = ({
50
52
  const [loading, setLoading] = useState(false);
51
53
  const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] = useState(false);
52
54
  const [saving, setSaving] = useState(false);
55
+ const [deleting, setDeleting] = useState(false);
56
+ const [restoring, setRestoring] = useState(false);
53
57
  const [error, setError] = useState("");
54
58
  const [isFolderPath, setIsFolderPath] = useState(false);
55
59
  const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
@@ -83,6 +87,7 @@ export const useFileViewer = ({
83
87
  const { loadedFilePathRef, restoredSelectionPathRef } = useFileLoader({
84
88
  hasSelectedPath,
85
89
  normalizedPath,
90
+ isDiffView,
86
91
  isSqliteFile,
87
92
  sqliteSelectedTable,
88
93
  sqliteTableOffset,
@@ -111,7 +116,7 @@ export const useFileViewer = ({
111
116
  viewScrollRatioRef,
112
117
  });
113
118
 
114
- const { diffLoading, diffError, diffContent } = useFileDiff({
119
+ const { diffLoading, diffError, diffContent, diffStatus } = useFileDiff({
115
120
  hasSelectedPath,
116
121
  isDiffView,
117
122
  isPreviewOnly,
@@ -131,6 +136,15 @@ export const useFileViewer = ({
131
136
  matchesBrowsePolicyPath(kProtectedBrowsePaths, normalizedPolicyPath);
132
137
  const isProtectedLocked = isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
133
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;
134
148
  const syntaxKind = useMemo(() => getFileSyntaxKind(normalizedPath), [normalizedPath]);
135
149
  const isMarkdownFile = syntaxKind === "markdown";
136
150
  const shouldUseHighlightedEditor = syntaxKind !== "plain";
@@ -187,6 +201,10 @@ export const useFileViewer = ({
187
201
  }
188
202
  }, [isMarkdownFile, viewMode]);
189
203
 
204
+ useEffect(() => {
205
+ setProtectedEditBypassPaths(new Set());
206
+ }, [normalizedPath]);
207
+
190
208
  useEffect(() => {
191
209
  try {
192
210
  window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
@@ -216,6 +234,7 @@ export const useFileViewer = ({
216
234
 
217
235
  useEditorSelectionRestore({
218
236
  canEditFile,
237
+ isEditBlocked,
219
238
  loading,
220
239
  hasSelectedPath,
221
240
  normalizedPath,
@@ -246,7 +265,6 @@ export const useFileViewer = ({
246
265
  detail: { path: normalizedPath },
247
266
  }),
248
267
  );
249
- showToast("Saved", "success");
250
268
  } catch (saveError) {
251
269
  const message = saveError.message || "Could not save file";
252
270
  setError(message);
@@ -256,6 +274,72 @@ export const useFileViewer = ({
256
274
  }
257
275
  }, [canEditFile, saving, isDirty, isEditBlocked, normalizedPath, content]);
258
276
 
277
+ const handleDelete = useCallback(async () => {
278
+ if (!canDeleteFile) return;
279
+ setDeleting(true);
280
+ setError("");
281
+ try {
282
+ const data = await deleteBrowseFile(normalizedPath);
283
+ const deletedPath = String(data?.path || normalizedPath);
284
+ setExternalChangeNoticeShown(false);
285
+ clearStoredFileDraft(normalizedPath);
286
+ updateDraftIndex(normalizedPath, false, {
287
+ dispatchEvent: (event) => window.dispatchEvent(event),
288
+ });
289
+ window.dispatchEvent(
290
+ new CustomEvent("alphaclaw:browse-file-saved", {
291
+ detail: { path: deletedPath },
292
+ }),
293
+ );
294
+ window.dispatchEvent(
295
+ new CustomEvent("alphaclaw:browse-file-deleted", {
296
+ detail: { path: deletedPath },
297
+ }),
298
+ );
299
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
300
+ showToast("File deleted", "success");
301
+ onRequestClearSelection();
302
+ } catch (deleteError) {
303
+ const message = deleteError.message || "Could not delete file";
304
+ setError(message);
305
+ if (/path is not a file/i.test(message)) {
306
+ showToast("Only files can be deleted", "warning");
307
+ onRequestClearSelection();
308
+ } else {
309
+ showToast(message, "error");
310
+ }
311
+ } finally {
312
+ setDeleting(false);
313
+ }
314
+ }, [canDeleteFile, normalizedPath, onRequestClearSelection]);
315
+
316
+ const handleRestore = useCallback(async () => {
317
+ if (!isDiffView || !diffStatus?.isDeleted || restoring) return;
318
+ setRestoring(true);
319
+ try {
320
+ const data = await restoreBrowseFile(normalizedPath);
321
+ const restoredPath = String(data?.path || normalizedPath);
322
+ window.dispatchEvent(
323
+ new CustomEvent("alphaclaw:browse-file-saved", {
324
+ detail: { path: restoredPath },
325
+ }),
326
+ );
327
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
328
+ showToast("File restored", "success");
329
+ onRequestEdit(restoredPath);
330
+ } catch (restoreError) {
331
+ showToast(restoreError.message || "Could not restore file", "error");
332
+ } finally {
333
+ setRestoring(false);
334
+ }
335
+ }, [
336
+ diffStatus?.isDeleted,
337
+ isDiffView,
338
+ normalizedPath,
339
+ onRequestEdit,
340
+ restoring,
341
+ ]);
342
+
259
343
  useFileViewerHotkeys({
260
344
  canEditFile,
261
345
  isPreviewOnly,
@@ -281,22 +365,56 @@ export const useFileViewer = ({
281
365
  });
282
366
  };
283
367
 
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) {
368
+ const persistDraftForContent = useCallback(
369
+ (nextContent, selection = null) => {
370
+ if (!hasSelectedPath || !canEditFile) return;
371
+ if (selection) {
372
+ writeStoredEditorSelection(normalizedPath, selection);
373
+ }
295
374
  writeStoredFileDraft(normalizedPath, nextContent);
296
375
  updateDraftIndex(normalizedPath, nextContent !== initialContent, {
297
376
  dispatchEvent: (event) => window.dispatchEvent(event),
298
377
  });
299
- }
378
+ },
379
+ [hasSelectedPath, canEditFile, normalizedPath, initialContent],
380
+ );
381
+
382
+ const handleContentInput = (event) => {
383
+ if (isEditBlocked || isPreviewOnly) return;
384
+ const nextContent = event.target.value;
385
+ setContent(nextContent);
386
+ persistDraftForContent(nextContent, {
387
+ start: event.target.selectionStart,
388
+ end: event.target.selectionEnd,
389
+ });
390
+ };
391
+
392
+ const handleEditorKeyDown = (event) => {
393
+ if (event.key !== "Tab") return;
394
+ if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
395
+ if (isEditBlocked || isPreviewOnly || !canEditFile) return;
396
+ const textareaElement = event.currentTarget;
397
+ if (!textareaElement) return;
398
+ event.preventDefault();
399
+ const start = Number(textareaElement.selectionStart || 0);
400
+ const end = Number(textareaElement.selectionEnd || 0);
401
+ textareaElement.setRangeText(" ", start, end, "end");
402
+ const nextContent = textareaElement.value;
403
+ setContent(nextContent);
404
+ persistDraftForContent(nextContent, {
405
+ start: textareaElement.selectionStart,
406
+ end: textareaElement.selectionEnd,
407
+ });
408
+ };
409
+
410
+ const handleDiscard = () => {
411
+ if (!canEditFile || !isDirty || saving || deleting) return;
412
+ setContent(initialContent);
413
+ clearStoredFileDraft(normalizedPath);
414
+ updateDraftIndex(normalizedPath, false, {
415
+ dispatchEvent: (event) => window.dispatchEvent(event),
416
+ });
417
+ showToast("Changes discarded", "info");
300
418
  };
301
419
 
302
420
  const handleEditorSelectionChange = () => {
@@ -315,6 +433,8 @@ export const useFileViewer = ({
315
433
  isPreviewOnly,
316
434
  loading,
317
435
  saving,
436
+ deleting,
437
+ restoring,
318
438
  showDelayedLoadingSpinner,
319
439
  error,
320
440
  isFolderPath,
@@ -333,6 +453,7 @@ export const useFileViewer = ({
333
453
  diffLoading,
334
454
  diffError,
335
455
  diffContent,
456
+ diffStatus,
336
457
  isMarkdownFile,
337
458
  frontmatterCollapsed,
338
459
  previewHtml,
@@ -343,6 +464,8 @@ export const useFileViewer = ({
343
464
  pathSegments,
344
465
  isDirty,
345
466
  canEditFile,
467
+ canDeleteFile,
468
+ isDeleteBlocked,
346
469
  isEditBlocked,
347
470
  isLockedFile,
348
471
  isProtectedFile,
@@ -366,8 +489,12 @@ export const useFileViewer = ({
366
489
  setSqliteTableOffset,
367
490
  handleChangeViewMode,
368
491
  handleSave,
492
+ handleDiscard,
493
+ handleDelete,
494
+ handleRestore,
369
495
  handleEditProtectedFile,
370
496
  handleContentInput,
497
+ handleEditorKeyDown,
371
498
  handleEditorScroll,
372
499
  handlePreviewScroll,
373
500
  handleEditorSelectionChange,
@@ -0,0 +1,131 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Badge } from "../badge.js";
4
+ import { ScopePicker } from "../scope-picker.js";
5
+ import { GmailWatchToggle } from "./gmail-watch-toggle.js";
6
+
7
+ const html = htm.bind(h);
8
+
9
+ const scopeListsEqual = (a = [], b = []) =>
10
+ a.length === b.length && a.every((scope) => b.includes(scope));
11
+
12
+ export const GoogleAccountRow = ({
13
+ account,
14
+ personal = false,
15
+ expanded,
16
+ onToggleExpanded,
17
+ scopes = [],
18
+ savedScopes = [],
19
+ apiStatus = {},
20
+ checkingApis = false,
21
+ onToggleScope,
22
+ onCheckApis,
23
+ onUpdatePermissions,
24
+ onEditCredentials,
25
+ onDisconnect,
26
+ gmailWatchStatus = null,
27
+ gmailWatchBusy = false,
28
+ onEnableGmailWatch,
29
+ onDisableGmailWatch,
30
+ onOpenGmailSetup,
31
+ onOpenGmailWebhook,
32
+ }) => {
33
+ const scopesChanged = !scopeListsEqual(scopes, savedScopes);
34
+ return html`
35
+ <div class="border border-border rounded-lg bg-black/20 overflow-visible">
36
+ <button
37
+ type="button"
38
+ onclick=${() => onToggleExpanded?.(account.id)}
39
+ class="w-full text-left px-3 py-2.5 flex items-center justify-between hover:bg-black/20"
40
+ >
41
+ <div class="min-w-0">
42
+ <div class="text-sm font-medium truncate">${account.email}</div>
43
+ </div>
44
+ <div class="flex items-center gap-2">
45
+ ${personal ? html`<${Badge} tone="neutral">Personal</${Badge}>` : null}
46
+ <${Badge} tone=${account.authenticated ? "success" : "warning"}>
47
+ ${account.authenticated ? "Connected" : "Awaiting sign-in"}
48
+ </${Badge}>
49
+ <span class="text-xs text-gray-500">${expanded ? "▾" : "▸"}</span>
50
+ </div>
51
+ </button>
52
+ ${expanded
53
+ ? html`
54
+ <div class="px-3 pb-3 space-y-3 border-t border-border">
55
+ <div class="flex justify-between items-center pt-3">
56
+ <span class="text-sm text-gray-400">Select permissions</span>
57
+ ${account.authenticated
58
+ ? html`<button
59
+ type="button"
60
+ onclick=${() => onCheckApis?.(account.id)}
61
+ disabled=${checkingApis}
62
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost disabled:opacity-50 disabled:cursor-not-allowed"
63
+ >
64
+ ${checkingApis ? "Checking APIs..." : "↻ Check APIs"}
65
+ </button>`
66
+ : null}
67
+ </div>
68
+ <${ScopePicker}
69
+ scopes=${scopes}
70
+ onToggle=${(scope) => onToggleScope?.(account.id, scope)}
71
+ apiStatus=${account.authenticated ? apiStatus : {}}
72
+ loading=${account.authenticated && checkingApis}
73
+ />
74
+ ${account.authenticated
75
+ ? html`
76
+ <div class="-mx-3 mt-4 mb-2 border-y border-border">
77
+ <div class="px-3 py-3 space-y-2">
78
+ <div class="flex justify-between items-center gap-2">
79
+ <span class="text-sm text-gray-400">Incoming events</span>
80
+ <button
81
+ type="button"
82
+ onclick=${() => onOpenGmailSetup?.(account.id)}
83
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
84
+ >
85
+ Configure
86
+ </button>
87
+ </div>
88
+ <${GmailWatchToggle}
89
+ account=${account}
90
+ watchStatus=${gmailWatchStatus}
91
+ busy=${gmailWatchBusy}
92
+ onEnable=${() => onEnableGmailWatch?.(account.id)}
93
+ onDisable=${() => onDisableGmailWatch?.(account.id)}
94
+ onOpenWebhook=${() => onOpenGmailWebhook?.()}
95
+ />
96
+ </div>
97
+ </div>
98
+ `
99
+ : null}
100
+ <div class="pt-1 space-y-2 sm:space-y-0 sm:flex sm:justify-between sm:items-center">
101
+ <div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
102
+ <button
103
+ type="button"
104
+ onclick=${() => onUpdatePermissions?.(account.id)}
105
+ disabled=${account.authenticated && !scopesChanged}
106
+ class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan disabled:opacity-50 disabled:cursor-not-allowed"
107
+ >
108
+ ${account.authenticated ? "Update Permissions" : "Sign in with Google"}
109
+ </button>
110
+ <button
111
+ type="button"
112
+ onclick=${() => onEditCredentials?.(account.id)}
113
+ class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
114
+ >
115
+ Edit Credentials
116
+ </button>
117
+ </div>
118
+ <button
119
+ type="button"
120
+ onclick=${() => onDisconnect?.(account.id)}
121
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost w-full sm:w-auto"
122
+ >
123
+ Disconnect
124
+ </button>
125
+ </div>
126
+ </div>
127
+ `
128
+ : null}
129
+ </div>
130
+ `;
131
+ };