@chrysb/alphaclaw 0.4.0 → 0.4.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +80 -5
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +3 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/toolbar.js +13 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
- package/lib/public/js/components/google/account-row.js +34 -1
- 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 +193 -44
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +5 -6
- package/lib/public/js/components/sidebar.js +3 -1
- package/lib/public/js/components/telegram-workspace/onboarding.js +1 -1
- 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 +180 -127
- package/lib/public/js/lib/api.js +106 -1
- package/lib/public/js/lib/format.js +71 -0
- package/lib/server/constants.js +27 -0
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +705 -0
- package/lib/server/google-state.js +130 -0
- package/lib/server/helpers.js +5 -7
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/openclaw.js +9 -1
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +19 -0
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +47 -14
- 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 +213 -64
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +1 -1
- package/package.json +1 -1
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ModalShell } from "../modal-shell.js";
|
|
5
|
+
import { PageHeader } from "../page-header.js";
|
|
6
|
+
import { CloseIcon } from "../icons.js";
|
|
7
|
+
import { ActionButton } from "../action-button.js";
|
|
8
|
+
import { fetchAgentSessions, sendAgentMessage } from "../../lib/api.js";
|
|
9
|
+
import { showToast } from "../toast.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
const copyText = async (value) => {
|
|
14
|
+
const text = String(value || "");
|
|
15
|
+
if (!text) return false;
|
|
16
|
+
try {
|
|
17
|
+
if (navigator?.clipboard?.writeText) {
|
|
18
|
+
await navigator.clipboard.writeText(text);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
try {
|
|
23
|
+
const element = document.createElement("textarea");
|
|
24
|
+
element.value = text;
|
|
25
|
+
element.setAttribute("readonly", "");
|
|
26
|
+
element.style.position = "fixed";
|
|
27
|
+
element.style.opacity = "0";
|
|
28
|
+
document.body.appendChild(element);
|
|
29
|
+
element.select();
|
|
30
|
+
document.execCommand("copy");
|
|
31
|
+
document.body.removeChild(element);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const kStepTitles = [
|
|
39
|
+
"Install + Authenticate gcloud",
|
|
40
|
+
"Enable APIs",
|
|
41
|
+
"Create Topic + IAM",
|
|
42
|
+
"Create Push Subscription",
|
|
43
|
+
"Build with your Agent",
|
|
44
|
+
];
|
|
45
|
+
const kTotalSteps = kStepTitles.length;
|
|
46
|
+
const kNoSessionSelectedValue = "__none__";
|
|
47
|
+
|
|
48
|
+
const renderCommandBlock = (command = "", onCopy = () => {}) => html`
|
|
49
|
+
<div class="rounded-lg border border-border bg-black/30 p-3">
|
|
50
|
+
<pre
|
|
51
|
+
class="pt-1 pl-2 text-[11px] leading-5 whitespace-pre-wrap break-all font-mono text-gray-300"
|
|
52
|
+
>
|
|
53
|
+
${command}</pre
|
|
54
|
+
>
|
|
55
|
+
<div class="pt-3">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onclick=${onCopy}
|
|
59
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
60
|
+
>
|
|
61
|
+
Copy
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export const GmailSetupWizard = ({
|
|
68
|
+
visible = false,
|
|
69
|
+
account = null,
|
|
70
|
+
clientConfig = null,
|
|
71
|
+
saving = false,
|
|
72
|
+
onClose = () => {},
|
|
73
|
+
onSaveSetup = async () => {},
|
|
74
|
+
onFinish = async () => {},
|
|
75
|
+
}) => {
|
|
76
|
+
const [step, setStep] = useState(0);
|
|
77
|
+
const [projectIdInput, setProjectIdInput] = useState("");
|
|
78
|
+
const [localError, setLocalError] = useState("");
|
|
79
|
+
const [projectIdResolved, setProjectIdResolved] = useState(false);
|
|
80
|
+
const [watchEnabled, setWatchEnabled] = useState(false);
|
|
81
|
+
const [sendingToAgent, setSendingToAgent] = useState(false);
|
|
82
|
+
const [agentMessageSent, setAgentMessageSent] = useState(false);
|
|
83
|
+
const [agentSessions, setAgentSessions] = useState([]);
|
|
84
|
+
const [selectedSessionKey, setSelectedSessionKey] = useState("");
|
|
85
|
+
const [loadingAgentSessions, setLoadingAgentSessions] = useState(false);
|
|
86
|
+
const [agentSessionsError, setAgentSessionsError] = useState("");
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!visible) return;
|
|
90
|
+
setStep(0);
|
|
91
|
+
setLocalError("");
|
|
92
|
+
setProjectIdInput("");
|
|
93
|
+
setProjectIdResolved(false);
|
|
94
|
+
setWatchEnabled(false);
|
|
95
|
+
setSendingToAgent(false);
|
|
96
|
+
setAgentMessageSent(false);
|
|
97
|
+
setAgentSessions([]);
|
|
98
|
+
setSelectedSessionKey("");
|
|
99
|
+
setLoadingAgentSessions(false);
|
|
100
|
+
setAgentSessionsError("");
|
|
101
|
+
}, [visible, account?.id]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!visible) return;
|
|
105
|
+
let active = true;
|
|
106
|
+
const loadAgentSessions = async () => {
|
|
107
|
+
try {
|
|
108
|
+
setLoadingAgentSessions(true);
|
|
109
|
+
setAgentSessionsError("");
|
|
110
|
+
const data = await fetchAgentSessions();
|
|
111
|
+
if (!active) return;
|
|
112
|
+
const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
113
|
+
setAgentSessions(sessions);
|
|
114
|
+
const defaultSession = sessions.find((sessionRow) => {
|
|
115
|
+
const key = String(sessionRow?.key || "").toLowerCase();
|
|
116
|
+
return key.includes(":direct:") || key.includes(":group:");
|
|
117
|
+
});
|
|
118
|
+
setSelectedSessionKey((currentKey) => currentKey || String(defaultSession?.key || ""));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (!active) return;
|
|
121
|
+
setAgentSessions([]);
|
|
122
|
+
setSelectedSessionKey("");
|
|
123
|
+
setAgentSessionsError(err.message || "Could not load sessions");
|
|
124
|
+
} finally {
|
|
125
|
+
if (active) setLoadingAgentSessions(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
loadAgentSessions();
|
|
129
|
+
return () => {
|
|
130
|
+
active = false;
|
|
131
|
+
};
|
|
132
|
+
}, [visible, account?.id]);
|
|
133
|
+
|
|
134
|
+
const commands = clientConfig?.commands || null;
|
|
135
|
+
const hasProjectIdFromConfig = Boolean(
|
|
136
|
+
String(clientConfig?.projectId || "").trim() || commands,
|
|
137
|
+
);
|
|
138
|
+
const needsProjectId = !hasProjectIdFromConfig && !projectIdResolved;
|
|
139
|
+
const detectedProjectId =
|
|
140
|
+
String(clientConfig?.projectId || "").trim() ||
|
|
141
|
+
String(projectIdInput || "").trim() ||
|
|
142
|
+
"<project-id>";
|
|
143
|
+
const client =
|
|
144
|
+
String(account?.client || clientConfig?.client || "default").trim() ||
|
|
145
|
+
"default";
|
|
146
|
+
|
|
147
|
+
const canAdvance = useMemo(() => {
|
|
148
|
+
if (needsProjectId) {
|
|
149
|
+
return String(projectIdInput || "").trim().length > 0;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}, [needsProjectId, projectIdInput]);
|
|
153
|
+
const selectableAgentSessions = useMemo(
|
|
154
|
+
() =>
|
|
155
|
+
agentSessions.filter((sessionRow) => {
|
|
156
|
+
const key = String(sessionRow?.key || "").toLowerCase();
|
|
157
|
+
return key.includes(":direct:") || key.includes(":group:");
|
|
158
|
+
}),
|
|
159
|
+
[agentSessions],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const handleCopy = async (value) => {
|
|
163
|
+
const ok = await copyText(value);
|
|
164
|
+
if (ok) {
|
|
165
|
+
showToast("Copied to clipboard", "success");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
showToast("Could not copy text", "error");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleFinish = async () => {
|
|
172
|
+
try {
|
|
173
|
+
setLocalError("");
|
|
174
|
+
await onFinish({
|
|
175
|
+
client,
|
|
176
|
+
projectId: String(projectIdInput || "").trim(),
|
|
177
|
+
});
|
|
178
|
+
setWatchEnabled(true);
|
|
179
|
+
setStep((prev) => Math.min(prev + 1, kTotalSteps - 1));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
setLocalError(err.message || "Could not finish setup");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleNext = async () => {
|
|
186
|
+
if (saving) return;
|
|
187
|
+
if (needsProjectId) {
|
|
188
|
+
if (!canAdvance) return;
|
|
189
|
+
setLocalError("");
|
|
190
|
+
try {
|
|
191
|
+
await onSaveSetup({
|
|
192
|
+
client,
|
|
193
|
+
projectId: String(projectIdInput || "").trim(),
|
|
194
|
+
});
|
|
195
|
+
setProjectIdResolved(true);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
setLocalError(err.message || "Could not save project id");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setStep((prev) => Math.min(prev + 1, kTotalSteps - 1));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleSendToAgent = async () => {
|
|
206
|
+
if (sendingToAgent || agentMessageSent) return;
|
|
207
|
+
try {
|
|
208
|
+
setSendingToAgent(true);
|
|
209
|
+
const accountEmail = String(account?.email || "this account").trim() || "this account";
|
|
210
|
+
const message =
|
|
211
|
+
`I just enabled Gmail watch for "${accountEmail}", set up the webhook, ` +
|
|
212
|
+
`and created the transform file. Help me set up what I want to do ` +
|
|
213
|
+
`with incoming email.`;
|
|
214
|
+
await sendAgentMessage({
|
|
215
|
+
message,
|
|
216
|
+
sessionKey: selectedSessionKey,
|
|
217
|
+
});
|
|
218
|
+
setAgentMessageSent(true);
|
|
219
|
+
showToast("Message sent to your agent", "success");
|
|
220
|
+
} catch (err) {
|
|
221
|
+
showToast(err.message || "Could not send message to agent", "error");
|
|
222
|
+
} finally {
|
|
223
|
+
setSendingToAgent(false);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return html`
|
|
228
|
+
<${ModalShell}
|
|
229
|
+
visible=${visible}
|
|
230
|
+
onClose=${onClose}
|
|
231
|
+
closeOnOverlayClick=${false}
|
|
232
|
+
closeOnEscape=${false}
|
|
233
|
+
panelClassName="relative bg-modal border border-border rounded-xl p-6 max-w-2xl w-full space-y-4"
|
|
234
|
+
>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onclick=${onClose}
|
|
238
|
+
class="absolute top-6 right-6 h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
239
|
+
aria-label="Close modal"
|
|
240
|
+
>
|
|
241
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
242
|
+
</button>
|
|
243
|
+
<div class="text-xs text-gray-500">Gmail Pub / Sub Setup</div>
|
|
244
|
+
<div class="flex items-center gap-1">
|
|
245
|
+
${kStepTitles.map(
|
|
246
|
+
(title, idx) => html`
|
|
247
|
+
<div
|
|
248
|
+
class=${`h-1 flex-1 rounded-full transition-colors ${idx <= step ? "bg-accent" : "bg-border"}`}
|
|
249
|
+
style=${idx <= step ? "background: var(--accent)" : ""}
|
|
250
|
+
title=${title}
|
|
251
|
+
></div>
|
|
252
|
+
`,
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
<${PageHeader}
|
|
256
|
+
title=${`Step ${step + 1} of ${kTotalSteps}: ${kStepTitles[step]}`}
|
|
257
|
+
actions=${null}
|
|
258
|
+
/>
|
|
259
|
+
${localError ? html`<div class="text-xs text-red-400">${localError}</div>` : null}
|
|
260
|
+
${
|
|
261
|
+
needsProjectId
|
|
262
|
+
? html`
|
|
263
|
+
<div
|
|
264
|
+
class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
|
|
265
|
+
>
|
|
266
|
+
<div class="text-sm">Project ID required</div>
|
|
267
|
+
<div class="text-xs text-gray-500">
|
|
268
|
+
Find it in the${" "}
|
|
269
|
+
<a
|
|
270
|
+
href="https://console.cloud.google.com/home/dashboard"
|
|
271
|
+
target="_blank"
|
|
272
|
+
rel="noreferrer"
|
|
273
|
+
class="ac-tip-link"
|
|
274
|
+
>
|
|
275
|
+
Google Cloud Console Project Selector
|
|
276
|
+
</a>
|
|
277
|
+
</div>
|
|
278
|
+
<input
|
|
279
|
+
type="text"
|
|
280
|
+
value=${projectIdInput}
|
|
281
|
+
oninput=${(event) => setProjectIdInput(event.target.value)}
|
|
282
|
+
class="w-full bg-black/30 border border-border rounded-lg px-2.5 py-2 text-xs font-mono focus:border-gray-500 focus:outline-none"
|
|
283
|
+
placeholder="my-gcp-project"
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
`
|
|
287
|
+
: null
|
|
288
|
+
}
|
|
289
|
+
${
|
|
290
|
+
!needsProjectId && step === 0
|
|
291
|
+
? html`
|
|
292
|
+
<div class="space-y-1">
|
|
293
|
+
<div class="text-xs text-gray-500">
|
|
294
|
+
If <code>gcloud</code> is not installed on your computer,
|
|
295
|
+
follow the official install guide:${" "}
|
|
296
|
+
<a
|
|
297
|
+
href="https://docs.cloud.google.com/sdk/docs/install-sdk"
|
|
298
|
+
target="_blank"
|
|
299
|
+
rel="noreferrer"
|
|
300
|
+
class="ac-tip-link"
|
|
301
|
+
>
|
|
302
|
+
Google Cloud SDK install docs
|
|
303
|
+
</a>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
${renderCommandBlock(
|
|
307
|
+
`gcloud auth login\n` +
|
|
308
|
+
`gcloud config set project ${detectedProjectId}`,
|
|
309
|
+
() =>
|
|
310
|
+
handleCopy(
|
|
311
|
+
`gcloud auth login\n` +
|
|
312
|
+
`gcloud config set project ${detectedProjectId}`,
|
|
313
|
+
),
|
|
314
|
+
)}
|
|
315
|
+
`
|
|
316
|
+
: null
|
|
317
|
+
}
|
|
318
|
+
${
|
|
319
|
+
!needsProjectId && step === 1
|
|
320
|
+
? renderCommandBlock(commands?.enableApis || "", () =>
|
|
321
|
+
handleCopy(commands?.enableApis || ""),
|
|
322
|
+
)
|
|
323
|
+
: null
|
|
324
|
+
}
|
|
325
|
+
${
|
|
326
|
+
!needsProjectId && step === 2
|
|
327
|
+
? html`
|
|
328
|
+
${renderCommandBlock(
|
|
329
|
+
`${commands?.createTopic || ""}\n\n${commands?.grantPublisher || ""}`.trim(),
|
|
330
|
+
() =>
|
|
331
|
+
handleCopy(
|
|
332
|
+
`${commands?.createTopic || ""}\n\n${commands?.grantPublisher || ""}`.trim(),
|
|
333
|
+
),
|
|
334
|
+
)}
|
|
335
|
+
`
|
|
336
|
+
: null
|
|
337
|
+
}
|
|
338
|
+
${
|
|
339
|
+
!needsProjectId && step === 3
|
|
340
|
+
? renderCommandBlock(commands?.createSubscription || "", () =>
|
|
341
|
+
handleCopy(commands?.createSubscription || ""),
|
|
342
|
+
)
|
|
343
|
+
: null
|
|
344
|
+
}
|
|
345
|
+
${
|
|
346
|
+
step === 4
|
|
347
|
+
? html`
|
|
348
|
+
<div
|
|
349
|
+
class="rounded-lg border border-border bg-black/20 p-3 space-y-3"
|
|
350
|
+
>
|
|
351
|
+
<div class="pt-1 space-y-1">
|
|
352
|
+
<div class="text-sm">Continue with your agent</div>
|
|
353
|
+
<div class="text-xs text-gray-500">
|
|
354
|
+
Tell your OpenClaw agent about what you want to build with
|
|
355
|
+
incoming email to continue the setup.
|
|
356
|
+
</div>
|
|
357
|
+
<div class="pt-2 space-y-2">
|
|
358
|
+
<div class="text-[11px] text-gray-500">Send this to session</div>
|
|
359
|
+
<div class="flex items-center gap-2">
|
|
360
|
+
<select
|
|
361
|
+
value=${selectedSessionKey || kNoSessionSelectedValue}
|
|
362
|
+
oninput=${(event) => {
|
|
363
|
+
const nextValue = String(event.target.value || "");
|
|
364
|
+
setSelectedSessionKey(
|
|
365
|
+
nextValue === kNoSessionSelectedValue ? "" : nextValue,
|
|
366
|
+
);
|
|
367
|
+
}}
|
|
368
|
+
disabled=${loadingAgentSessions || sendingToAgent || agentMessageSent}
|
|
369
|
+
class="flex-1 min-w-0 bg-black/30 border border-border rounded-lg px-2.5 py-2 text-xs font-mono focus:border-gray-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
|
370
|
+
>
|
|
371
|
+
${!selectedSessionKey
|
|
372
|
+
? html`<option value=${kNoSessionSelectedValue}>Select a session...</option>`
|
|
373
|
+
: null}
|
|
374
|
+
${selectableAgentSessions.map(
|
|
375
|
+
(sessionRow) => html`
|
|
376
|
+
<option value=${sessionRow.key}>
|
|
377
|
+
${sessionRow.label || sessionRow.key}
|
|
378
|
+
</option>
|
|
379
|
+
`,
|
|
380
|
+
)}
|
|
381
|
+
</select>
|
|
382
|
+
<${ActionButton}
|
|
383
|
+
onClick=${handleSendToAgent}
|
|
384
|
+
disabled=${!selectedSessionKey || agentMessageSent}
|
|
385
|
+
loading=${sendingToAgent}
|
|
386
|
+
loadingMode="inline"
|
|
387
|
+
idleLabel=${agentMessageSent ? "Sent" : "Send to Agent"}
|
|
388
|
+
loadingLabel="Sending..."
|
|
389
|
+
tone="primary"
|
|
390
|
+
size="sm"
|
|
391
|
+
className="h-[34px] px-3"
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
${loadingAgentSessions
|
|
395
|
+
? html`<div class="text-[11px] text-gray-500">Loading sessions...</div>`
|
|
396
|
+
: null}
|
|
397
|
+
${agentSessionsError
|
|
398
|
+
? html`<div class="text-[11px] text-red-400">${agentSessionsError}</div>`
|
|
399
|
+
: null}
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
`
|
|
404
|
+
: null
|
|
405
|
+
}
|
|
406
|
+
<div class="grid grid-cols-2 gap-2 pt-2">
|
|
407
|
+
${step === 0
|
|
408
|
+
? html`<div></div>`
|
|
409
|
+
: html`<${ActionButton}
|
|
410
|
+
onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
|
|
411
|
+
disabled=${saving}
|
|
412
|
+
idleLabel="Back"
|
|
413
|
+
tone="secondary"
|
|
414
|
+
size="md"
|
|
415
|
+
className="w-full justify-center"
|
|
416
|
+
/>`}
|
|
417
|
+
${
|
|
418
|
+
step < kTotalSteps - 2
|
|
419
|
+
? html`<${ActionButton}
|
|
420
|
+
onClick=${handleNext}
|
|
421
|
+
disabled=${saving || (needsProjectId && !canAdvance)}
|
|
422
|
+
idleLabel="Next"
|
|
423
|
+
tone="primary"
|
|
424
|
+
size="md"
|
|
425
|
+
className="w-full justify-center"
|
|
426
|
+
/>`
|
|
427
|
+
: step === kTotalSteps - 2
|
|
428
|
+
? html`<${ActionButton}
|
|
429
|
+
onClick=${handleFinish}
|
|
430
|
+
disabled=${false}
|
|
431
|
+
loading=${saving}
|
|
432
|
+
idleLabel="Enable watch"
|
|
433
|
+
loadingLabel="Enabling..."
|
|
434
|
+
tone="primary"
|
|
435
|
+
size="md"
|
|
436
|
+
className="w-full justify-center"
|
|
437
|
+
/>`
|
|
438
|
+
: html`<${ActionButton}
|
|
439
|
+
onClick=${onClose}
|
|
440
|
+
disabled=${saving || sendingToAgent}
|
|
441
|
+
idleLabel="Done"
|
|
442
|
+
tone="secondary"
|
|
443
|
+
size="md"
|
|
444
|
+
className="w-full justify-center"
|
|
445
|
+
/>`
|
|
446
|
+
}
|
|
447
|
+
</div>
|
|
448
|
+
</${ModalShell}>
|
|
449
|
+
`;
|
|
450
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Badge } from "../badge.js";
|
|
4
|
+
import { ToggleSwitch } from "../toggle-switch.js";
|
|
5
|
+
import { InfoTooltip } from "../info-tooltip.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
const resolveWatchState = ({ watchStatus, busy = false }) => {
|
|
10
|
+
if (busy) return { label: "Starting", tone: "warning" };
|
|
11
|
+
if (!watchStatus?.enabled) return { label: "Stopped", tone: "neutral" };
|
|
12
|
+
if (watchStatus.enabled && !watchStatus.running)
|
|
13
|
+
return { label: "Error", tone: "danger" };
|
|
14
|
+
return { label: "Watching", tone: "success" };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const GmailWatchToggle = ({
|
|
18
|
+
account,
|
|
19
|
+
watchStatus = null,
|
|
20
|
+
busy = false,
|
|
21
|
+
onEnable = () => {},
|
|
22
|
+
onDisable = () => {},
|
|
23
|
+
onOpenWebhook = () => {},
|
|
24
|
+
}) => {
|
|
25
|
+
const hasGmailReadScope = Array.isArray(account?.activeScopes)
|
|
26
|
+
? account.activeScopes.includes("gmail:read")
|
|
27
|
+
: Array.isArray(account?.services)
|
|
28
|
+
? account.services.includes("gmail:read")
|
|
29
|
+
: false;
|
|
30
|
+
if (!hasGmailReadScope) {
|
|
31
|
+
return html`
|
|
32
|
+
<div class="bg-black/30 rounded-lg px-3 py-2">
|
|
33
|
+
<div class="text-xs text-gray-500">
|
|
34
|
+
Gmail watch requires <code>gmail:read</code>. Add it in permissions
|
|
35
|
+
above, then update permissions.
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state = resolveWatchState({ watchStatus, busy });
|
|
42
|
+
const enabled = Boolean(watchStatus?.enabled);
|
|
43
|
+
return html`
|
|
44
|
+
<div
|
|
45
|
+
class="flex items-center justify-between bg-black/30 border border-transparent rounded-lg px-3 py-2 cursor-pointer hover:bg-black/40 hover:border-white/20 transition-colors"
|
|
46
|
+
role="button"
|
|
47
|
+
tabindex="0"
|
|
48
|
+
onClick=${() => onOpenWebhook?.()}
|
|
49
|
+
onKeyDown=${(event) => {
|
|
50
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
onOpenWebhook?.();
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<div class="flex items-center gap-1.5 text-sm">
|
|
56
|
+
<span>🔔 Gmail</span>
|
|
57
|
+
<${InfoTooltip}
|
|
58
|
+
text="Watches this inbox for new email events and routes them to your agent via the Gmail hook."
|
|
59
|
+
widthClass="w-72"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div
|
|
63
|
+
class="flex items-center gap-2"
|
|
64
|
+
onClick=${(event) => event.stopPropagation()}
|
|
65
|
+
onKeyDown=${(event) => event.stopPropagation()}
|
|
66
|
+
>
|
|
67
|
+
<${Badge} tone=${state.tone}>${state.label}</${Badge}>
|
|
68
|
+
<${ToggleSwitch}
|
|
69
|
+
checked=${enabled}
|
|
70
|
+
disabled=${busy}
|
|
71
|
+
label=""
|
|
72
|
+
onChange=${(nextChecked) => {
|
|
73
|
+
if (busy) return;
|
|
74
|
+
if (nextChecked) onEnable?.();
|
|
75
|
+
else onDisable?.();
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
`;
|
|
81
|
+
};
|