@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.
- package/bin/alphaclaw.js +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- 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
|
+
};
|