@chrysb/alphaclaw 0.4.0 → 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 (47) hide show
  1. package/lib/public/css/shell.css +21 -19
  2. package/lib/public/css/theme.css +17 -0
  3. package/lib/public/js/app.js +80 -5
  4. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  5. package/lib/public/js/components/file-viewer/index.js +3 -0
  6. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  7. package/lib/public/js/components/file-viewer/toolbar.js +13 -0
  8. package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
  9. package/lib/public/js/components/google/account-row.js +34 -1
  10. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  11. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  12. package/lib/public/js/components/google/index.js +118 -4
  13. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  14. package/lib/public/js/components/scope-picker.js +1 -1
  15. package/lib/public/js/components/sidebar-git-panel.js +5 -6
  16. package/lib/public/js/components/sidebar.js +2 -0
  17. package/lib/public/js/components/toast.js +11 -7
  18. package/lib/public/js/components/usage-tab/constants.js +31 -0
  19. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  20. package/lib/public/js/components/usage-tab/index.js +72 -0
  21. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  22. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  23. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  24. package/lib/public/js/components/webhooks.js +182 -129
  25. package/lib/public/js/lib/api.js +106 -1
  26. package/lib/public/js/lib/format.js +71 -0
  27. package/lib/server/constants.js +28 -0
  28. package/lib/server/gmail-push.js +109 -0
  29. package/lib/server/gmail-serve.js +254 -0
  30. package/lib/server/gmail-watch.js +725 -0
  31. package/lib/server/google-state.js +130 -0
  32. package/lib/server/helpers.js +5 -7
  33. package/lib/server/internal-files-migration.js +31 -3
  34. package/lib/server/routes/gmail.js +128 -0
  35. package/lib/server/routes/google.js +19 -0
  36. package/lib/server/routes/system.js +107 -0
  37. package/lib/server/routes/usage.js +29 -2
  38. package/lib/server/routes/webhooks.js +52 -17
  39. package/lib/server/usage-db.js +283 -15
  40. package/lib/server/watchdog.js +66 -0
  41. package/lib/server/webhook-middleware.js +99 -1
  42. package/lib/server/webhooks.js +214 -65
  43. package/lib/server.js +27 -0
  44. package/lib/setup/gitignore +3 -0
  45. package/lib/setup/hourly-git-sync.sh +1 -1
  46. package/package.json +1 -1
  47. package/lib/public/js/components/usage-tab.js +0 -531
@@ -11,35 +11,37 @@
11
11
  }
12
12
 
13
13
  .global-restart-banner {
14
- grid-column: 1 / -1;
15
- grid-row: 1;
16
- background: rgba(234, 179, 8, 0.08);
17
- border-bottom: 1px solid rgba(234, 179, 8, 0.32);
18
- padding: 10px 32px;
14
+ position: fixed;
15
+ left: 50%;
16
+ bottom: 52px;
17
+ transform: translateX(-50%);
18
+ width: auto;
19
+ max-width: calc(100vw - 32px);
20
+ z-index: 40;
21
+ pointer-events: none;
19
22
  }
20
23
 
21
24
  .global-restart-banner__content {
22
- margin: 0 auto;
23
- max-width: 900px;
24
- position: relative;
25
+ border: 1px solid rgba(234, 179, 8, 0.35);
26
+ background: rgba(43, 32, 6, 0.95);
27
+ box-shadow: 0 18px 46px rgba(0, 0, 0, 0.42);
28
+ border-radius: 14px;
29
+ padding: 10px 14px;
25
30
  display: flex;
26
31
  align-items: center;
27
- justify-content: center;
32
+ justify-content: space-between;
28
33
  gap: 12px;
29
- min-height: 28px;
34
+ pointer-events: auto;
30
35
  }
31
36
 
32
37
  .global-restart-banner__text {
33
38
  font-size: 12px;
34
39
  color: #fde68a;
35
- text-align: center;
40
+ line-height: 1.4;
36
41
  }
37
42
 
38
43
  .global-restart-banner__button {
39
- position: absolute;
40
- right: 0;
41
- top: 50%;
42
- transform: translateY(-50%);
44
+ flex-shrink: 0;
43
45
  }
44
46
 
45
47
  .app-content {
@@ -292,13 +294,13 @@
292
294
  grid-template-rows: auto 1fr 24px;
293
295
  }
294
296
  .global-restart-banner {
295
- padding: 10px 14px;
297
+ max-width: calc(100vw - 20px);
298
+ bottom: 44px;
296
299
  }
297
300
  .global-restart-banner__content {
298
- max-width: none;
299
- align-items: flex-start;
301
+ align-items: stretch;
300
302
  flex-direction: column;
301
- min-height: 0;
303
+ gap: 8px;
302
304
  }
303
305
  .global-restart-banner__text {
304
306
  text-align: left;
@@ -66,6 +66,13 @@ body::before {
66
66
  background: rgba(0, 0, 0, 0.12);
67
67
  }
68
68
 
69
+ /* Shared inset panel for "surface on surface" layouts. */
70
+ .ac-surface-inset {
71
+ border: 1px solid var(--panel-border-contrast);
72
+ border-radius: 10px;
73
+ background: rgba(0, 0, 0, 0.12);
74
+ }
75
+
69
76
  .ac-history-summary {
70
77
  cursor: pointer;
71
78
  list-style: none;
@@ -116,6 +123,16 @@ body::before {
116
123
  border-color: var(--panel-border-contrast) !important;
117
124
  }
118
125
 
126
+ .ac-tip-link {
127
+ color: var(--accent-link);
128
+ text-decoration: underline;
129
+ text-underline-offset: 2px;
130
+ }
131
+
132
+ .ac-tip-link:hover {
133
+ color: var(--accent);
134
+ }
135
+
119
136
  /* Universal field contrast treatment (all tabs/pages). */
120
137
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
121
138
  select,
@@ -43,7 +43,7 @@ import { LoadingSpinner } from "./components/loading-spinner.js";
43
43
  import { WatchdogTab } from "./components/watchdog-tab.js";
44
44
  import { FileViewer } from "./components/file-viewer/index.js";
45
45
  import { AppSidebar } from "./components/sidebar.js";
46
- import { UsageTab } from "./components/usage-tab.js";
46
+ import { UsageTab } from "./components/usage-tab/index.js";
47
47
  import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
48
48
  const html = htm.bind(h);
49
49
  const kDefaultUiTab = "general";
@@ -52,7 +52,32 @@ const kSidebarMinWidthPx = 180;
52
52
  const kSidebarMaxWidthPx = 460;
53
53
  const kBrowseLastPathUiSettingKey = "browseLastPath";
54
54
  const kLastMenuRouteUiSettingKey = "lastMenuRoute";
55
+ const kBrowseRestartRequiredRules = [
56
+ { type: "file", path: "openclaw.json" },
57
+ { type: "directory", path: "hooks/transforms" },
58
+ ];
55
59
  const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
60
+ const normalizeRestartRulePath = (value) =>
61
+ String(value || "")
62
+ .trim()
63
+ .replace(/^\/+|\/+$/g, "");
64
+ const matchesBrowseRestartRequiredRule = (path, rule) => {
65
+ const normalizedPath = normalizeRestartRulePath(path);
66
+ if (!normalizedPath) return false;
67
+ if (!rule || typeof rule !== "object") return false;
68
+ const type = String(rule.type || "").toLowerCase();
69
+ const targetPath = normalizeRestartRulePath(rule.path);
70
+ if (!targetPath) return false;
71
+ if (type === "directory") {
72
+ return normalizedPath === targetPath || normalizedPath.startsWith(`${targetPath}/`);
73
+ }
74
+ if (type === "file") {
75
+ return normalizedPath === targetPath;
76
+ }
77
+ return false;
78
+ };
79
+ const shouldRequireRestartForBrowsePath = (path) =>
80
+ kBrowseRestartRequiredRules.some((rule) => matchesBrowseRestartRequiredRule(path, rule));
56
81
 
57
82
  const clampSidebarWidth = (value) =>
58
83
  Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
@@ -99,6 +124,7 @@ const GeneralTab = ({
99
124
  onRefreshStatuses = () => {},
100
125
  onSwitchTab,
101
126
  onNavigate,
127
+ onOpenGmailWebhook = () => {},
102
128
  isActive,
103
129
  restartingGateway,
104
130
  onRestartGateway,
@@ -106,6 +132,7 @@ const GeneralTab = ({
106
132
  openclawUpdateInProgress = false,
107
133
  onOpenclawVersionActionComplete = () => {},
108
134
  onOpenclawUpdate,
135
+ onRestartRequired = () => {},
109
136
  }) => {
110
137
  const [dashboardLoading, setDashboardLoading] = useState(false);
111
138
  const [repairingWatchdog, setRepairingWatchdog] = useState(false);
@@ -279,7 +306,11 @@ const GeneralTab = ({
279
306
  onReject=${handleReject}
280
307
  />
281
308
  <${Features} onSwitchTab=${onSwitchTab} />
282
- <${Google} gatewayStatus=${gatewayStatus} />
309
+ <${Google}
310
+ gatewayStatus=${gatewayStatus}
311
+ onRestartRequired=${onRestartRequired}
312
+ onOpenGmailWebhook=${onOpenGmailWebhook}
313
+ />
283
314
 
284
315
  ${repo &&
285
316
  html`
@@ -420,11 +451,13 @@ const App = () => {
420
451
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
421
452
  const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
422
453
  const [restartRequired, setRestartRequired] = useState(false);
454
+ const [browseRestartRequired, setBrowseRestartRequired] = useState(false);
423
455
  const [restartingGateway, setRestartingGateway] = useState(false);
424
456
  const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
425
457
  const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
426
458
  const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
427
459
  const menuRef = useRef(null);
460
+ const routeHistoryRef = useRef([]);
428
461
  const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
429
462
  enabled: onboarded === true,
430
463
  });
@@ -433,6 +466,7 @@ const App = () => {
433
466
  });
434
467
  const sharedStatus = sharedStatusPoll.data || null;
435
468
  const sharedWatchdogStatus = sharedWatchdogPoll.data?.status || null;
469
+ const isAnyRestartRequired = restartRequired || browseRestartRequired;
436
470
  const refreshSharedStatuses = useCallback(() => {
437
471
  sharedStatusPoll.refresh();
438
472
  sharedWatchdogPoll.refresh();
@@ -536,6 +570,18 @@ const App = () => {
536
570
  return () => clearInterval(id);
537
571
  }, [onboarded, restartRequired, restartingGateway, refreshRestartStatus]);
538
572
 
573
+ useEffect(() => {
574
+ const handleBrowseFileSaved = (event) => {
575
+ const savedPath = String(event?.detail?.path || "");
576
+ if (!shouldRequireRestartForBrowsePath(savedPath)) return;
577
+ setBrowseRestartRequired(true);
578
+ };
579
+ window.addEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
580
+ return () => {
581
+ window.removeEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
582
+ };
583
+ }, []);
584
+
539
585
  const handleGatewayRestart = useCallback(async () => {
540
586
  if (restartingGateway) return;
541
587
  setRestartingGateway(true);
@@ -543,6 +589,7 @@ const App = () => {
543
589
  const data = await restartGateway();
544
590
  if (!data?.ok) throw new Error(data?.error || "Gateway restart failed");
545
591
  setRestartRequired(!!data.restartRequired);
592
+ setBrowseRestartRequired(false);
546
593
  setGatewayRestartSignal(Date.now());
547
594
  refreshSharedStatuses();
548
595
  showToast("Gateway restarted", "success");
@@ -782,6 +829,16 @@ const App = () => {
782
829
  setBrowsePreviewPath("");
783
830
  }, [location]);
784
831
 
832
+ useEffect(() => {
833
+ const historyStack = routeHistoryRef.current;
834
+ const lastEntry = historyStack[historyStack.length - 1];
835
+ if (lastEntry === location) return;
836
+ historyStack.push(location);
837
+ if (historyStack.length > 100) {
838
+ historyStack.shift();
839
+ }
840
+ }, [location]);
841
+
785
842
  useEffect(() => {
786
843
  if (location.startsWith("/browse")) return;
787
844
  if (location === "/telegram") return;
@@ -865,8 +922,24 @@ const App = () => {
865
922
  <${Webhooks}
866
923
  selectedHookName=${hookName}
867
924
  onSelectHook=${(name) => setLocation(`/webhooks/${encodeURIComponent(name)}`)}
868
- onBackToList=${() => setLocation("/webhooks")}
925
+ onBackToList=${() => {
926
+ const historyStack = routeHistoryRef.current;
927
+ const hasPreviousRoute = historyStack.length > 1;
928
+ if (!hasPreviousRoute) {
929
+ setLocation("/webhooks");
930
+ return;
931
+ }
932
+ const currentPath = getHashPath();
933
+ window.history.back();
934
+ window.setTimeout(() => {
935
+ if (getHashPath() === currentPath) {
936
+ setLocation("/webhooks");
937
+ }
938
+ }, 180);
939
+ }}
869
940
  onRestartRequired=${setRestartRequired}
941
+ onOpenFile=${(relativePath) =>
942
+ navigateToBrowseFile(String(relativePath || "").trim(), { view: "edit" })}
870
943
  />
871
944
  </div>
872
945
  `;
@@ -878,7 +951,7 @@ const App = () => {
878
951
  style=${{ "--sidebar-width": `${sidebarWidthPx}px` }}
879
952
  >
880
953
  <${GlobalRestartBanner}
881
- visible=${restartRequired}
954
+ visible=${isAnyRestartRequired}
882
955
  restarting=${restartingGateway}
883
956
  onRestart=${handleGatewayRestart}
884
957
  />
@@ -969,6 +1042,7 @@ const App = () => {
969
1042
  onRefreshStatuses=${refreshSharedStatuses}
970
1043
  onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
971
1044
  onNavigate=${navigateToSubScreen}
1045
+ onOpenGmailWebhook=${() => setLocation("/webhooks/gmail")}
972
1046
  isActive=${location === "/general"}
973
1047
  restartingGateway=${restartingGateway}
974
1048
  onRestartGateway=${handleGatewayRestart}
@@ -976,6 +1050,7 @@ const App = () => {
976
1050
  openclawUpdateInProgress=${openclawUpdateInProgress}
977
1051
  onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
978
1052
  onOpenclawUpdate=${handleOpenclawUpdate}
1053
+ onRestartRequired=${setRestartRequired}
979
1054
  />
980
1055
  </div>
981
1056
  </div>
@@ -1050,7 +1125,7 @@ const App = () => {
1050
1125
  </div>
1051
1126
  </div>
1052
1127
  <${ToastContainer}
1053
- className="fixed bottom-10 right-4 z-50 space-y-2 pointer-events-none"
1128
+ className="fixed top-4 right-4 z-50 space-y-2 pointer-events-none"
1054
1129
  />
1055
1130
  </div>
1056
1131
 
@@ -8,6 +8,7 @@ const EditorTextarea = ({
8
8
  editorTextareaRef,
9
9
  renderContent,
10
10
  handleContentInput,
11
+ handleEditorKeyDown,
11
12
  handleEditorScroll,
12
13
  handleEditorSelectionChange,
13
14
  isEditBlocked,
@@ -18,6 +19,7 @@ const EditorTextarea = ({
18
19
  ref=${editorTextareaRef}
19
20
  value=${renderContent}
20
21
  onInput=${handleContentInput}
22
+ onKeyDown=${handleEditorKeyDown}
21
23
  onScroll=${handleEditorScroll}
22
24
  onSelect=${handleEditorSelectionChange}
23
25
  onKeyUp=${handleEditorSelectionChange}
@@ -48,6 +50,7 @@ export const EditorSurface = ({
48
50
  editorTextareaRef,
49
51
  renderContent,
50
52
  handleContentInput,
53
+ handleEditorKeyDown,
51
54
  handleEditorScroll,
52
55
  handleEditorSelectionChange,
53
56
  isEditBlocked,
@@ -97,6 +100,7 @@ export const EditorSurface = ({
97
100
  editorTextareaRef=${editorTextareaRef}
98
101
  renderContent=${renderContent}
99
102
  handleContentInput=${handleContentInput}
103
+ handleEditorKeyDown=${handleEditorKeyDown}
100
104
  handleEditorScroll=${handleEditorScroll}
101
105
  handleEditorSelectionChange=${handleEditorSelectionChange}
102
106
  isEditBlocked=${isEditBlocked}
@@ -110,6 +114,7 @@ export const EditorSurface = ({
110
114
  editorTextareaRef=${editorTextareaRef}
111
115
  renderContent=${renderContent}
112
116
  handleContentInput=${handleContentInput}
117
+ handleEditorKeyDown=${handleEditorKeyDown}
113
118
  handleEditorScroll=${handleEditorScroll}
114
119
  handleEditorSelectionChange=${handleEditorSelectionChange}
115
120
  isEditBlocked=${isEditBlocked}
@@ -52,6 +52,7 @@ export const FileViewer = ({
52
52
  viewMode=${state.viewMode}
53
53
  handleChangeViewMode=${actions.handleChangeViewMode}
54
54
  handleSave=${actions.handleSave}
55
+ handleDiscard=${actions.handleDiscard}
55
56
  loading=${state.loading}
56
57
  canEditFile=${derived.canEditFile}
57
58
  isEditBlocked=${derived.isEditBlocked}
@@ -153,6 +154,7 @@ export const FileViewer = ({
153
154
  editorTextareaRef=${refs.editorTextareaRef}
154
155
  renderContent=${state.renderContent}
155
156
  handleContentInput=${actions.handleContentInput}
157
+ handleEditorKeyDown=${actions.handleEditorKeyDown}
156
158
  handleEditorScroll=${actions.handleEditorScroll}
157
159
  handleEditorSelectionChange=${actions.handleEditorSelectionChange}
158
160
  isEditBlocked=${derived.isEditBlocked}
@@ -171,6 +173,7 @@ export const FileViewer = ({
171
173
  editorTextareaRef=${refs.editorTextareaRef}
172
174
  renderContent=${state.renderContent}
173
175
  handleContentInput=${actions.handleContentInput}
176
+ handleEditorKeyDown=${actions.handleEditorKeyDown}
174
177
  handleEditorScroll=${actions.handleEditorScroll}
175
178
  handleEditorSelectionChange=${actions.handleEditorSelectionChange}
176
179
  isEditBlocked=${derived.isEditBlocked}
@@ -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}
@@ -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,
@@ -84,6 +85,18 @@ export const FileViewerToolbar = ({
84
85
  </button>
85
86
  `
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}
87
100
  <${ActionButton}
88
101
  onClick=${handleSave}
89
102
  disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
@@ -265,7 +265,6 @@ export const useFileViewer = ({
265
265
  detail: { path: normalizedPath },
266
266
  }),
267
267
  );
268
- showToast("Saved", "success");
269
268
  } catch (saveError) {
270
269
  const message = saveError.message || "Could not save file";
271
270
  setError(message);
@@ -366,22 +365,56 @@ export const useFileViewer = ({
366
365
  });
367
366
  };
368
367
 
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) {
368
+ const persistDraftForContent = useCallback(
369
+ (nextContent, selection = null) => {
370
+ if (!hasSelectedPath || !canEditFile) return;
371
+ if (selection) {
372
+ writeStoredEditorSelection(normalizedPath, selection);
373
+ }
380
374
  writeStoredFileDraft(normalizedPath, nextContent);
381
375
  updateDraftIndex(normalizedPath, nextContent !== initialContent, {
382
376
  dispatchEvent: (event) => window.dispatchEvent(event),
383
377
  });
384
- }
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");
385
418
  };
386
419
 
387
420
  const handleEditorSelectionChange = () => {
@@ -456,10 +489,12 @@ export const useFileViewer = ({
456
489
  setSqliteTableOffset,
457
490
  handleChangeViewMode,
458
491
  handleSave,
492
+ handleDiscard,
459
493
  handleDelete,
460
494
  handleRestore,
461
495
  handleEditProtectedFile,
462
496
  handleContentInput,
497
+ handleEditorKeyDown,
463
498
  handleEditorScroll,
464
499
  handlePreviewScroll,
465
500
  handleEditorSelectionChange,
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
3
  import { Badge } from "../badge.js";
4
4
  import { ScopePicker } from "../scope-picker.js";
5
+ import { GmailWatchToggle } from "./gmail-watch-toggle.js";
5
6
 
6
7
  const html = htm.bind(h);
7
8
 
@@ -22,10 +23,16 @@ export const GoogleAccountRow = ({
22
23
  onUpdatePermissions,
23
24
  onEditCredentials,
24
25
  onDisconnect,
26
+ gmailWatchStatus = null,
27
+ gmailWatchBusy = false,
28
+ onEnableGmailWatch,
29
+ onDisableGmailWatch,
30
+ onOpenGmailSetup,
31
+ onOpenGmailWebhook,
25
32
  }) => {
26
33
  const scopesChanged = !scopeListsEqual(scopes, savedScopes);
27
34
  return html`
28
- <div class="border border-border rounded-lg bg-black/20 overflow-hidden">
35
+ <div class="border border-border rounded-lg bg-black/20 overflow-visible">
29
36
  <button
30
37
  type="button"
31
38
  onclick=${() => onToggleExpanded?.(account.id)}
@@ -64,6 +71,32 @@ export const GoogleAccountRow = ({
64
71
  apiStatus=${account.authenticated ? apiStatus : {}}
65
72
  loading=${account.authenticated && checkingApis}
66
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}
67
100
  <div class="pt-1 space-y-2 sm:space-y-0 sm:flex sm:justify-between sm:items-center">
68
101
  <div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
69
102
  <button