@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
@@ -0,0 +1,553 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import {
5
+ checkGoogleApis,
6
+ disconnectGoogle,
7
+ fetchGoogleCredentials,
8
+ saveGoogleAccount,
9
+ } from "../../lib/api.js";
10
+ import { getDefaultScopes, toggleScopeLogic } from "../scope-picker.js";
11
+ import { CredentialsModal } from "../credentials-modal.js";
12
+ import { ConfirmDialog } from "../confirm-dialog.js";
13
+ import { showToast } from "../toast.js";
14
+ import { PageHeader } from "../page-header.js";
15
+ import { ActionButton } from "../action-button.js";
16
+ import { GoogleAccountRow } from "./account-row.js";
17
+ import { AddGoogleAccountModal } from "./add-account-modal.js";
18
+ import { useGoogleAccounts } from "./use-google-accounts.js";
19
+ import { useGmailWatch } from "./use-gmail-watch.js";
20
+ import { GmailSetupWizard } from "./gmail-setup-wizard.js";
21
+
22
+ const html = htm.bind(h);
23
+
24
+ const hasScopesChanged = (nextScopes = [], savedScopes = []) =>
25
+ nextScopes.length !== savedScopes.length ||
26
+ nextScopes.some((scope) => !savedScopes.includes(scope));
27
+
28
+ const isPersonalAccount = (account = {}) => Boolean(account.personal);
29
+
30
+ const kGoogleIconPath = "/assets/icons/google_icon.svg";
31
+
32
+ export const Google = ({
33
+ gatewayStatus,
34
+ onRestartRequired = () => {},
35
+ onOpenGmailWebhook = () => {},
36
+ }) => {
37
+ const {
38
+ accounts,
39
+ loading,
40
+ hasCompanyCredentials,
41
+ refreshAccounts,
42
+ } = useGoogleAccounts({ gatewayStatus });
43
+ const [expandedAccountId, setExpandedAccountId] = useState("");
44
+ const [scopesByAccountId, setScopesByAccountId] = useState({});
45
+ const [savedScopesByAccountId, setSavedScopesByAccountId] = useState({});
46
+ const [apiStatusByAccountId, setApiStatusByAccountId] = useState({});
47
+ const [checkingByAccountId, setCheckingByAccountId] = useState({});
48
+ const [addMenuOpen, setAddMenuOpen] = useState(false);
49
+ const [credentialsModalState, setCredentialsModalState] = useState({
50
+ visible: false,
51
+ accountId: "",
52
+ client: "default",
53
+ personal: false,
54
+ title: "Connect Google Workspace",
55
+ submitLabel: "Connect Google",
56
+ defaultInstrType: "workspace",
57
+ initialValues: {},
58
+ });
59
+ const [addCompanyModalOpen, setAddCompanyModalOpen] = useState(false);
60
+ const [savingAddCompany, setSavingAddCompany] = useState(false);
61
+ const [disconnectAccountId, setDisconnectAccountId] = useState("");
62
+ const [gmailWizardState, setGmailWizardState] = useState({
63
+ visible: false,
64
+ accountId: "",
65
+ });
66
+ const {
67
+ loading: gmailLoading,
68
+ watchByAccountId,
69
+ clientConfigByClient,
70
+ busyByAccountId,
71
+ savingClient,
72
+ refresh: refreshGmailWatch,
73
+ saveClientSetup,
74
+ startWatchForAccount,
75
+ stopWatchForAccount,
76
+ } = useGmailWatch({ gatewayStatus, accounts });
77
+
78
+ const hasPersonalAccount = useMemo(
79
+ () => accounts.some((account) => isPersonalAccount(account)),
80
+ [accounts],
81
+ );
82
+ const hasCompanyAccount = useMemo(
83
+ () => accounts.some((account) => !isPersonalAccount(account)),
84
+ [accounts],
85
+ );
86
+
87
+ const getAccountById = useCallback(
88
+ (accountId) => accounts.find((account) => account.id === accountId) || null,
89
+ [accounts],
90
+ );
91
+
92
+ const ensureScopesForAccount = useCallback((account) => {
93
+ const nextScopes = Array.isArray(account.activeScopes) && account.activeScopes.length
94
+ ? account.activeScopes
95
+ : Array.isArray(account.services) && account.services.length
96
+ ? account.services
97
+ : getDefaultScopes();
98
+ setSavedScopesByAccountId((prev) => ({ ...prev, [account.id]: [...nextScopes] }));
99
+ setScopesByAccountId((prev) => {
100
+ const current = prev[account.id];
101
+ if (!current || !hasScopesChanged(current, nextScopes)) {
102
+ return { ...prev, [account.id]: [...nextScopes] };
103
+ }
104
+ return prev;
105
+ });
106
+ }, []);
107
+
108
+ useEffect(() => {
109
+ if (!accounts.length) {
110
+ setExpandedAccountId("");
111
+ return;
112
+ }
113
+ const firstAwaitingSignInId =
114
+ accounts.find((account) => !account.authenticated)?.id || "";
115
+ setExpandedAccountId((previousId) => {
116
+ if (previousId && accounts.some((account) => account.id === previousId)) {
117
+ return previousId;
118
+ }
119
+ return firstAwaitingSignInId;
120
+ });
121
+ accounts.forEach((account) => ensureScopesForAccount(account));
122
+ }, [accounts, ensureScopesForAccount]);
123
+
124
+ const startAuth = useCallback(
125
+ (accountId) => {
126
+ const account = getAccountById(accountId);
127
+ if (!account) return;
128
+ const scopes = scopesByAccountId[accountId] || account.activeScopes || getDefaultScopes();
129
+ if (!scopes.length) {
130
+ window.alert("Select at least one service");
131
+ return;
132
+ }
133
+ const authUrl =
134
+ `/auth/google/start?accountId=${encodeURIComponent(accountId)}` +
135
+ `&services=${encodeURIComponent(scopes.join(","))}&_ts=${Date.now()}`;
136
+ const popup = window.open(
137
+ authUrl,
138
+ `google-auth-${accountId}`,
139
+ "popup=yes,width=500,height=700",
140
+ );
141
+ if (!popup || popup.closed) window.location.href = authUrl;
142
+ },
143
+ [getAccountById, scopesByAccountId],
144
+ );
145
+
146
+ const handleToggleScope = (accountId, scope) => {
147
+ setScopesByAccountId((prev) => ({
148
+ ...prev,
149
+ [accountId]: toggleScopeLogic(prev[accountId] || [], scope),
150
+ }));
151
+ };
152
+
153
+ const handleCheckApis = useCallback(async (accountId) => {
154
+ setApiStatusByAccountId((prev) => {
155
+ const next = { ...prev };
156
+ delete next[accountId];
157
+ return next;
158
+ });
159
+ setCheckingByAccountId({ [accountId]: true });
160
+ try {
161
+ const data = await checkGoogleApis(accountId);
162
+ if (data.results) {
163
+ setApiStatusByAccountId((prev) => ({ ...prev, [accountId]: data.results }));
164
+ }
165
+ } finally {
166
+ setCheckingByAccountId((prev) => {
167
+ if (!prev[accountId]) return prev;
168
+ const next = { ...prev };
169
+ delete next[accountId];
170
+ return next;
171
+ });
172
+ }
173
+ }, []);
174
+
175
+ useEffect(() => {
176
+ const handler = async (event) => {
177
+ if (event.data?.google === "success") {
178
+ showToast("✓ Google account connected", "success");
179
+ const accountId = String(event.data?.accountId || "").trim();
180
+ setApiStatusByAccountId({});
181
+ await refreshAccounts();
182
+ await refreshGmailWatch();
183
+ if (accountId) {
184
+ await handleCheckApis(accountId);
185
+ }
186
+ } else if (event.data?.google === "error") {
187
+ showToast(`✗ Google auth failed: ${event.data.message || "unknown"}`, "error");
188
+ }
189
+ };
190
+ window.addEventListener("message", handler);
191
+ return () => window.removeEventListener("message", handler);
192
+ }, [handleCheckApis, refreshAccounts, refreshGmailWatch]);
193
+
194
+ useEffect(() => {
195
+ if (!expandedAccountId) return;
196
+ const account = getAccountById(expandedAccountId);
197
+ if (!account?.authenticated) return;
198
+ if (checkingByAccountId[expandedAccountId]) return;
199
+ if (apiStatusByAccountId[expandedAccountId]) return;
200
+ handleCheckApis(expandedAccountId);
201
+ }, [
202
+ accounts,
203
+ apiStatusByAccountId,
204
+ checkingByAccountId,
205
+ expandedAccountId,
206
+ getAccountById,
207
+ handleCheckApis,
208
+ ]);
209
+
210
+ const handleDisconnect = async (accountId) => {
211
+ const data = await disconnectGoogle(accountId);
212
+ if (!data.ok) {
213
+ showToast(`Failed to disconnect: ${data.error || "unknown"}`, "error");
214
+ return;
215
+ }
216
+ showToast("Google account disconnected", "success");
217
+ setApiStatusByAccountId((prev) => {
218
+ const next = { ...prev };
219
+ delete next[accountId];
220
+ return next;
221
+ });
222
+ await refreshAccounts();
223
+ await refreshGmailWatch();
224
+ };
225
+
226
+ const openCredentialsModal = ({
227
+ accountId = "",
228
+ client = "default",
229
+ personal = false,
230
+ title = "Connect Google Workspace",
231
+ submitLabel = "Connect Google",
232
+ defaultInstrType = personal ? "personal" : "workspace",
233
+ initialValues = {},
234
+ }) => {
235
+ setCredentialsModalState({
236
+ visible: true,
237
+ accountId,
238
+ client,
239
+ personal,
240
+ title,
241
+ submitLabel,
242
+ defaultInstrType,
243
+ initialValues,
244
+ });
245
+ };
246
+
247
+ const closeCredentialsModal = () => {
248
+ setCredentialsModalState((prev) => ({ ...prev, visible: false }));
249
+ };
250
+
251
+ const handleCredentialsSaved = async (account) => {
252
+ await refreshAccounts();
253
+ if (account?.id) startAuth(account.id);
254
+ };
255
+
256
+ const handleAddCompanyAccount = async ({ email, setError }) => {
257
+ setSavingAddCompany(true);
258
+ try {
259
+ const data = await saveGoogleAccount({
260
+ email,
261
+ client: "default",
262
+ personal: false,
263
+ services: getDefaultScopes(),
264
+ });
265
+ if (!data.ok) {
266
+ setError?.(data.error || "Could not add account");
267
+ return;
268
+ }
269
+ setAddCompanyModalOpen(false);
270
+ await refreshAccounts();
271
+ if (data.accountId) startAuth(data.accountId);
272
+ } finally {
273
+ setSavingAddCompany(false);
274
+ }
275
+ };
276
+
277
+ const handleAddCompanyClick = () => {
278
+ setAddMenuOpen(false);
279
+ if (hasCompanyAccount && hasCompanyCredentials) {
280
+ setAddCompanyModalOpen(true);
281
+ return;
282
+ }
283
+ openCredentialsModal({
284
+ client: "default",
285
+ personal: false,
286
+ title: "Add Company Account",
287
+ submitLabel: "Save Credentials",
288
+ defaultInstrType: "workspace",
289
+ });
290
+ };
291
+
292
+ const handleAddPersonalClick = () => {
293
+ setAddMenuOpen(false);
294
+ openCredentialsModal({
295
+ client: "personal",
296
+ personal: true,
297
+ title: "Add Personal Account",
298
+ submitLabel: "Save Credentials",
299
+ defaultInstrType: "personal",
300
+ });
301
+ };
302
+
303
+ const handleEditCredentials = async (accountId) => {
304
+ const account = getAccountById(accountId);
305
+ if (!account) return;
306
+ const personal = isPersonalAccount(account);
307
+ const client = personal ? "personal" : (account.client || "default");
308
+ let credentialValues = {};
309
+ try {
310
+ const credentialResponse = await fetchGoogleCredentials({
311
+ accountId: account.id,
312
+ client,
313
+ });
314
+ if (credentialResponse?.ok) {
315
+ credentialValues = {
316
+ clientId: String(credentialResponse.clientId || ""),
317
+ clientSecret: String(credentialResponse.clientSecret || ""),
318
+ };
319
+ }
320
+ } catch {
321
+ showToast("Could not load saved client credentials", "warning");
322
+ }
323
+ openCredentialsModal({
324
+ accountId: account.id,
325
+ client,
326
+ personal,
327
+ title: `Edit Credentials (${account.email})`,
328
+ submitLabel: "Save Credentials",
329
+ defaultInstrType: personal ? "personal" : "workspace",
330
+ initialValues: {
331
+ email: account.email,
332
+ ...credentialValues,
333
+ },
334
+ });
335
+ };
336
+
337
+ const openGmailSetupWizard = (accountId) => {
338
+ setGmailWizardState({
339
+ visible: true,
340
+ accountId: String(accountId || ""),
341
+ });
342
+ };
343
+
344
+ const closeGmailSetupWizard = () => {
345
+ setGmailWizardState({
346
+ visible: false,
347
+ accountId: "",
348
+ });
349
+ };
350
+
351
+ const handleEnableGmailWatch = async (accountId) => {
352
+ const account = getAccountById(accountId);
353
+ if (!account) return;
354
+ const client = String(account.client || "default").trim() || "default";
355
+ const clientConfig = clientConfigByClient.get(client);
356
+ if (!clientConfig?.configured) {
357
+ openGmailSetupWizard(accountId);
358
+ return;
359
+ }
360
+ try {
361
+ const result = await startWatchForAccount(accountId);
362
+ if (result?.restartRequired) {
363
+ onRestartRequired(true);
364
+ }
365
+ showToast("Gmail watch enabled", "success");
366
+ } catch (err) {
367
+ showToast(err.message || "Could not enable Gmail watch", "error");
368
+ }
369
+ };
370
+
371
+ const handleDisableGmailWatch = async (accountId) => {
372
+ try {
373
+ await stopWatchForAccount(accountId);
374
+ showToast("Gmail watch disabled", "info");
375
+ } catch (err) {
376
+ showToast(err.message || "Could not disable Gmail watch", "error");
377
+ }
378
+ };
379
+
380
+ const handleFinishGmailSetupWizard = async ({ client, projectId }) => {
381
+ const accountId = String(gmailWizardState.accountId || "").trim();
382
+ if (!accountId) return;
383
+ await saveClientSetup({
384
+ client,
385
+ projectId,
386
+ regeneratePushToken: false,
387
+ });
388
+ await startWatchForAccount(accountId);
389
+ showToast("Gmail setup complete and watch enabled", "success");
390
+ };
391
+
392
+ const renderEmptyState = () => html`
393
+ <div class="text-center space-y-2 py-1">
394
+ <div class="rounded-lg border border-border bg-black/20 px-3 py-5">
395
+ <div class="flex flex-col items-center justify-center gap-1.5">
396
+ <img
397
+ src=${kGoogleIconPath}
398
+ alt="Google logo"
399
+ class="h-4 w-4 shrink-0"
400
+ loading="lazy"
401
+ decoding="async"
402
+ />
403
+ <p class="text-xs text-gray-500">
404
+ Connect Gmail, Calendar, Contacts, Drive, Sheets, Tasks, Docs, and Meet.
405
+ </p>
406
+ </div>
407
+ </div>
408
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
409
+ <${ActionButton}
410
+ onClick=${handleAddCompanyClick}
411
+ tone="primary"
412
+ size="sm"
413
+ idleLabel="Add Company Account"
414
+ className="w-full font-medium"
415
+ />
416
+ <${ActionButton}
417
+ onClick=${handleAddPersonalClick}
418
+ tone="secondary"
419
+ size="sm"
420
+ idleLabel="Add Personal Account"
421
+ className="w-full font-medium"
422
+ />
423
+ </div>
424
+ </div>
425
+ `;
426
+
427
+ return html`
428
+ <div class="bg-surface border border-border rounded-xl p-4">
429
+ <${PageHeader}
430
+ title="Google Accounts"
431
+ actions=${html`
432
+ ${accounts.length
433
+ ? html`
434
+ <div class="relative">
435
+ <button
436
+ type="button"
437
+ onclick=${() => setAddMenuOpen((prev) => !prev)}
438
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
439
+ >
440
+ + Add Account
441
+ </button>
442
+ ${addMenuOpen
443
+ ? html`
444
+ <div
445
+ class="absolute right-0 top-full mt-2 min-w-[210px] rounded-lg border border-border bg-modal p-1 z-20"
446
+ >
447
+ <button
448
+ type="button"
449
+ onclick=${handleAddCompanyClick}
450
+ class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
451
+ >
452
+ Company account
453
+ </button>
454
+ ${!hasPersonalAccount
455
+ ? html`<button
456
+ type="button"
457
+ onclick=${handleAddPersonalClick}
458
+ class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
459
+ >
460
+ Personal account
461
+ </button>`
462
+ : null}
463
+ </div>
464
+ `
465
+ : null}
466
+ </div>
467
+ `
468
+ : null}
469
+ `}
470
+ />
471
+ ${loading
472
+ ? html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`
473
+ : accounts.length
474
+ ? html`
475
+ <div class="space-y-2 mt-3">
476
+ ${accounts.map((account) =>
477
+ html`<${GoogleAccountRow}
478
+ key=${account.id}
479
+ account=${account}
480
+ personal=${isPersonalAccount(account)}
481
+ expanded=${expandedAccountId === account.id}
482
+ onToggleExpanded=${(accountId) =>
483
+ setExpandedAccountId((prev) => (prev === accountId ? "" : accountId))}
484
+ scopes=${scopesByAccountId[account.id] || account.activeScopes || getDefaultScopes()}
485
+ savedScopes=${savedScopesByAccountId[account.id] || account.activeScopes || getDefaultScopes()}
486
+ apiStatus=${apiStatusByAccountId[account.id] || {}}
487
+ checkingApis=${expandedAccountId === account.id && Boolean(checkingByAccountId[account.id])}
488
+ onToggleScope=${handleToggleScope}
489
+ onCheckApis=${handleCheckApis}
490
+ onUpdatePermissions=${(accountId) => startAuth(accountId)}
491
+ onEditCredentials=${handleEditCredentials}
492
+ onDisconnect=${(accountId) => setDisconnectAccountId(accountId)}
493
+ gmailWatchStatus=${watchByAccountId.get(account.id) || null}
494
+ gmailWatchBusy=${Boolean(busyByAccountId[account.id])}
495
+ onEnableGmailWatch=${handleEnableGmailWatch}
496
+ onDisableGmailWatch=${handleDisableGmailWatch}
497
+ onOpenGmailSetup=${openGmailSetupWizard}
498
+ onOpenGmailWebhook=${onOpenGmailWebhook}
499
+ />`,
500
+ )}
501
+ </div>
502
+ `
503
+ : renderEmptyState()}
504
+ </div>
505
+
506
+ <${CredentialsModal}
507
+ visible=${credentialsModalState.visible}
508
+ onClose=${closeCredentialsModal}
509
+ onSaved=${handleCredentialsSaved}
510
+ title=${credentialsModalState.title}
511
+ submitLabel=${credentialsModalState.submitLabel}
512
+ defaultInstrType=${credentialsModalState.defaultInstrType}
513
+ client=${credentialsModalState.client}
514
+ personal=${credentialsModalState.personal}
515
+ accountId=${credentialsModalState.accountId}
516
+ initialValues=${credentialsModalState.initialValues}
517
+ />
518
+
519
+ <${AddGoogleAccountModal}
520
+ visible=${addCompanyModalOpen}
521
+ onClose=${() => setAddCompanyModalOpen(false)}
522
+ onSubmit=${handleAddCompanyAccount}
523
+ loading=${savingAddCompany}
524
+ title="Add Company Account"
525
+ />
526
+
527
+ <${GmailSetupWizard}
528
+ visible=${gmailWizardState.visible}
529
+ account=${getAccountById(gmailWizardState.accountId)}
530
+ clientConfig=${clientConfigByClient.get(
531
+ String(getAccountById(gmailWizardState.accountId)?.client || "default").trim() || "default",
532
+ ) || null}
533
+ saving=${savingClient || gmailLoading}
534
+ onClose=${closeGmailSetupWizard}
535
+ onSaveSetup=${saveClientSetup}
536
+ onFinish=${handleFinishGmailSetupWizard}
537
+ />
538
+
539
+ <${ConfirmDialog}
540
+ visible=${Boolean(disconnectAccountId)}
541
+ title="Disconnect Google account?"
542
+ message="Your agent will lose access to Gmail, Calendar, and other Google Workspace services until you reconnect."
543
+ confirmLabel="Disconnect"
544
+ cancelLabel="Cancel"
545
+ onCancel=${() => setDisconnectAccountId("")}
546
+ onConfirm=${async () => {
547
+ const accountId = disconnectAccountId;
548
+ setDisconnectAccountId("");
549
+ await handleDisconnect(accountId);
550
+ }}
551
+ />
552
+ `;
553
+ };
@@ -0,0 +1,140 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ fetchGmailConfig,
4
+ renewGmailWatch,
5
+ saveGmailConfig,
6
+ startGmailWatch,
7
+ stopGmailWatch,
8
+ } from "../../lib/api.js";
9
+
10
+ export const useGmailWatch = ({ gatewayStatus, accounts = [] }) => {
11
+ const [loading, setLoading] = useState(true);
12
+ const [config, setConfig] = useState(null);
13
+ const [busyByAccountId, setBusyByAccountId] = useState({});
14
+ const [savingClient, setSavingClient] = useState(false);
15
+
16
+ const refresh = useCallback(async () => {
17
+ setLoading(true);
18
+ try {
19
+ const nextConfig = await fetchGmailConfig();
20
+ setConfig(nextConfig);
21
+ return nextConfig;
22
+ } finally {
23
+ setLoading(false);
24
+ }
25
+ }, []);
26
+
27
+ useEffect(() => {
28
+ if (gatewayStatus !== "running") return;
29
+ refresh();
30
+ }, [gatewayStatus, refresh]);
31
+
32
+ useEffect(() => {
33
+ if (gatewayStatus !== "running") return;
34
+ if (!accounts.length) return;
35
+ refresh();
36
+ }, [accounts, gatewayStatus, refresh]);
37
+
38
+ const watchByAccountId = useMemo(() => {
39
+ const map = new Map();
40
+ for (const entry of config?.accounts || []) {
41
+ map.set(String(entry.accountId || ""), entry);
42
+ }
43
+ return map;
44
+ }, [config]);
45
+
46
+ const clientConfigByClient = useMemo(() => {
47
+ const map = new Map();
48
+ for (const clientConfig of config?.clients || []) {
49
+ map.set(String(clientConfig.client || "default"), clientConfig);
50
+ }
51
+ return map;
52
+ }, [config]);
53
+
54
+ const setBusy = (accountId, busy) => {
55
+ setBusyByAccountId((prev) => {
56
+ const key = String(accountId || "");
57
+ if (!key) return prev;
58
+ if (busy) return { ...prev, [key]: true };
59
+ if (!prev[key]) return prev;
60
+ const next = { ...prev };
61
+ delete next[key];
62
+ return next;
63
+ });
64
+ };
65
+
66
+ const startWatchForAccount = useCallback(async (accountId) => {
67
+ const key = String(accountId || "");
68
+ setBusy(key, true);
69
+ try {
70
+ const data = await startGmailWatch(key);
71
+ await refresh();
72
+ return data;
73
+ } finally {
74
+ setBusy(key, false);
75
+ }
76
+ }, [refresh]);
77
+
78
+ const stopWatchForAccount = useCallback(async (accountId) => {
79
+ const key = String(accountId || "");
80
+ setBusy(key, true);
81
+ try {
82
+ await stopGmailWatch(key);
83
+ await refresh();
84
+ } finally {
85
+ setBusy(key, false);
86
+ }
87
+ }, [refresh]);
88
+
89
+ const renewForAccount = useCallback(async (accountId = "") => {
90
+ const key = String(accountId || "");
91
+ if (key) setBusy(key, true);
92
+ try {
93
+ await renewGmailWatch({ accountId: key, force: true });
94
+ await refresh();
95
+ } finally {
96
+ if (key) setBusy(key, false);
97
+ }
98
+ }, [refresh]);
99
+
100
+ const saveClientSetup = useCallback(async ({
101
+ client = "default",
102
+ projectId = "",
103
+ regeneratePushToken = false,
104
+ } = {}) => {
105
+ setSavingClient(true);
106
+ try {
107
+ const data = await saveGmailConfig({
108
+ client,
109
+ projectId,
110
+ regeneratePushToken,
111
+ });
112
+ await refresh();
113
+ return data;
114
+ } catch (err) {
115
+ const message = String(err?.message || "");
116
+ if (message.toLowerCase().includes("not found")) {
117
+ throw new Error(
118
+ "Gmail watch API route not found. Restart AlphaClaw so /api/gmail routes are loaded.",
119
+ );
120
+ }
121
+ throw err;
122
+ } finally {
123
+ setSavingClient(false);
124
+ }
125
+ }, [refresh]);
126
+
127
+ return {
128
+ loading,
129
+ config,
130
+ watchByAccountId,
131
+ clientConfigByClient,
132
+ busyByAccountId,
133
+ savingClient,
134
+ refresh,
135
+ saveClientSetup,
136
+ startWatchForAccount,
137
+ stopWatchForAccount,
138
+ renewForAccount,
139
+ };
140
+ };