@chrysb/alphaclaw 0.6.0 → 0.6.2-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 (53) hide show
  1. package/lib/public/css/agents.css +1 -1
  2. package/lib/public/css/cron.css +535 -0
  3. package/lib/public/css/theme.css +72 -0
  4. package/lib/public/js/app.js +45 -10
  5. package/lib/public/js/components/action-button.js +26 -20
  6. package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
  7. package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
  8. package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
  9. package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
  10. package/lib/public/js/components/agents-tab/index.js +4 -0
  11. package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
  12. package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
  13. package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
  14. package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
  15. package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
  16. package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
  17. package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
  18. package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
  19. package/lib/public/js/components/cron-tab/index.js +100 -0
  20. package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
  21. package/lib/public/js/components/doctor/summary-cards.js +5 -11
  22. package/lib/public/js/components/google/gmail-setup-wizard.js +30 -30
  23. package/lib/public/js/components/google/index.js +1 -1
  24. package/lib/public/js/components/icons.js +13 -0
  25. package/lib/public/js/components/pill-tabs.js +33 -0
  26. package/lib/public/js/components/pop-actions.js +58 -0
  27. package/lib/public/js/components/routes/agents-route.js +4 -0
  28. package/lib/public/js/components/routes/cron-route.js +9 -0
  29. package/lib/public/js/components/routes/index.js +1 -0
  30. package/lib/public/js/components/segmented-control.js +15 -9
  31. package/lib/public/js/components/summary-stat-card.js +17 -0
  32. package/lib/public/js/components/tooltip.js +50 -4
  33. package/lib/public/js/components/watchdog-tab.js +46 -1
  34. package/lib/public/js/lib/api.js +94 -0
  35. package/lib/public/js/lib/app-navigation.js +2 -0
  36. package/lib/public/js/lib/storage-keys.js +1 -0
  37. package/lib/public/setup.html +1 -0
  38. package/lib/server/agents/agents.js +15 -0
  39. package/lib/server/constants.js +1 -0
  40. package/lib/server/cost-utils.js +312 -0
  41. package/lib/server/cron-service.js +461 -0
  42. package/lib/server/db/usage/index.js +100 -1
  43. package/lib/server/db/usage/pricing.js +1 -83
  44. package/lib/server/db/usage/sessions.js +4 -1
  45. package/lib/server/db/usage/shared.js +2 -1
  46. package/lib/server/db/usage/summary.js +5 -1
  47. package/lib/server/gmail-watch.js +0 -1
  48. package/lib/server/onboarding/index.js +39 -5
  49. package/lib/server/onboarding/openclaw.js +25 -19
  50. package/lib/server/onboarding/validation.js +28 -0
  51. package/lib/server/routes/cron.js +148 -0
  52. package/lib/server.js +13 -0
  53. package/package.json +1 -1
@@ -0,0 +1,366 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "https://esm.sh/preact/hooks";
8
+ import { usePolling } from "../../hooks/usePolling.js";
9
+ import {
10
+ fetchCronBulkRuns,
11
+ fetchCronBulkUsage,
12
+ fetchCronJobRuns,
13
+ fetchCronJobs,
14
+ fetchCronJobUsage,
15
+ fetchCronStatus,
16
+ setCronJobEnabled,
17
+ triggerCronJobRun,
18
+ updateCronJobPrompt,
19
+ } from "../../lib/api.js";
20
+ import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
21
+ import { showToast } from "../toast.js";
22
+ import { kAllCronJobsRouteKey } from "./cron-helpers.js";
23
+
24
+ const kDefaultListPanelWidthPx = 372;
25
+ const kListPanelMinWidthPx = 220;
26
+ const kListPanelMaxWidthPx = 480;
27
+ const kListPanelWidthUiSettingKey = "cronListPanelWidthPx";
28
+ const kRunsPageSize = 25;
29
+ const kCalendarUsageDays = 30;
30
+ const kCalendarPastDays = 30;
31
+
32
+ const clampListPanelWidth = (value) =>
33
+ Math.max(kListPanelMinWidthPx, Math.min(kListPanelMaxWidthPx, value));
34
+
35
+ const normalizeRouteJobId = (jobId = "") => {
36
+ const normalized = String(jobId || "").trim();
37
+ return normalized || kAllCronJobsRouteKey;
38
+ };
39
+
40
+ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
41
+ const listPanelRef = useRef(null);
42
+ const [listPanelWidthPx, setListPanelWidthPx] = useState(() => {
43
+ const settings = readUiSettings();
44
+ if (!Number.isFinite(settings?.[kListPanelWidthUiSettingKey])) {
45
+ return kDefaultListPanelWidthPx;
46
+ }
47
+ return clampListPanelWidth(settings[kListPanelWidthUiSettingKey]);
48
+ });
49
+ const [isResizingListPanel, setIsResizingListPanel] = useState(false);
50
+ const [runStatusFilter, setRunStatusFilter] = useState("all");
51
+ const [runDeliveryFilter, setRunDeliveryFilter] = useState("all");
52
+ const [runEntries, setRunEntries] = useState([]);
53
+ const [runHasMore, setRunHasMore] = useState(false);
54
+ const [runNextOffset, setRunNextOffset] = useState(0);
55
+ const [runTotal, setRunTotal] = useState(0);
56
+ const [loadingMoreRuns, setLoadingMoreRuns] = useState(false);
57
+ const [promptValue, setPromptValue] = useState("");
58
+ const [savedPromptValue, setSavedPromptValue] = useState("");
59
+ const [savingPrompt, setSavingPrompt] = useState(false);
60
+ const [runningJob, setRunningJob] = useState(false);
61
+ const [togglingJobEnabled, setTogglingJobEnabled] = useState(false);
62
+ const [usageDays, setUsageDays] = useState(30);
63
+
64
+ const selectedRouteKey = normalizeRouteJobId(jobId);
65
+ const selectedJobId =
66
+ selectedRouteKey === kAllCronJobsRouteKey ? "" : selectedRouteKey;
67
+
68
+ const jobsPoll = usePolling(
69
+ () => fetchCronJobs({ sortBy: "nextRunAtMs", sortDir: "asc" }),
70
+ 15000,
71
+ );
72
+ const statusPoll = usePolling(fetchCronStatus, 30000);
73
+ const runsPoll = usePolling(
74
+ () => {
75
+ if (!selectedJobId) {
76
+ return Promise.resolve({
77
+ ok: true,
78
+ runs: { entries: [], hasMore: false, nextOffset: 0 },
79
+ });
80
+ }
81
+ return fetchCronJobRuns(selectedJobId, {
82
+ limit: kRunsPageSize,
83
+ offset: 0,
84
+ status: runStatusFilter,
85
+ deliveryStatus: runDeliveryFilter,
86
+ sortDir: "desc",
87
+ });
88
+ },
89
+ 10000,
90
+ { enabled: !!selectedJobId },
91
+ );
92
+ const usagePoll = usePolling(
93
+ () => {
94
+ if (!selectedJobId) return Promise.resolve({ ok: true, usage: null });
95
+ return fetchCronJobUsage(selectedJobId, { days: usageDays });
96
+ },
97
+ 60000,
98
+ { enabled: !!selectedJobId },
99
+ );
100
+ const bulkUsagePoll = usePolling(
101
+ () => fetchCronBulkUsage({ days: kCalendarUsageDays }),
102
+ 60000,
103
+ { enabled: !selectedJobId },
104
+ );
105
+ const bulkRunsPoll = usePolling(
106
+ () =>
107
+ fetchCronBulkRuns({
108
+ sinceMs: Date.now() - kCalendarPastDays * 24 * 60 * 60 * 1000,
109
+ limitPerJob: 1200,
110
+ }),
111
+ 30000,
112
+ { enabled: !selectedJobId },
113
+ );
114
+
115
+ useEffect(() => {
116
+ const settings = readUiSettings();
117
+ settings[kListPanelWidthUiSettingKey] = listPanelWidthPx;
118
+ writeUiSettings(settings);
119
+ }, [listPanelWidthPx]);
120
+
121
+ useEffect(() => {
122
+ if (!runsPoll.data?.runs) return;
123
+ setRunEntries(
124
+ Array.isArray(runsPoll.data.runs.entries)
125
+ ? runsPoll.data.runs.entries
126
+ : [],
127
+ );
128
+ setRunHasMore(!!runsPoll.data.runs.hasMore);
129
+ setRunNextOffset(Number(runsPoll.data.runs.nextOffset || 0));
130
+ setRunTotal(Number(runsPoll.data.runs.total || 0));
131
+ }, [runsPoll.data]);
132
+
133
+ const jobs = useMemo(
134
+ () => (Array.isArray(jobsPoll.data?.jobs) ? jobsPoll.data.jobs : []),
135
+ [jobsPoll.data],
136
+ );
137
+
138
+ const selectedJob = useMemo(
139
+ () => jobs.find((job) => String(job?.id || "") === selectedJobId) || null,
140
+ [jobs, selectedJobId],
141
+ );
142
+
143
+ useEffect(() => {
144
+ if (!selectedJobId) {
145
+ setPromptValue("");
146
+ setSavedPromptValue("");
147
+ return;
148
+ }
149
+ const prompt = String(selectedJob?.payload?.message || "");
150
+ setPromptValue(prompt);
151
+ setSavedPromptValue(prompt);
152
+ }, [selectedJobId, selectedJob?.payload?.message]);
153
+
154
+ useEffect(() => {
155
+ setRunEntries([]);
156
+ setRunHasMore(false);
157
+ setRunNextOffset(0);
158
+ setRunTotal(0);
159
+ if (!selectedJobId) return;
160
+ runsPoll.refresh();
161
+ }, [selectedJobId, runStatusFilter, runDeliveryFilter]);
162
+
163
+ useEffect(() => {
164
+ if (!selectedJobId) return;
165
+ usagePoll.refresh();
166
+ }, [selectedJobId, usageDays]);
167
+
168
+ const resizeListPanelWithClientX = useCallback((clientX) => {
169
+ const listPanelElement = listPanelRef.current;
170
+ if (!listPanelElement) return;
171
+ const parentBounds =
172
+ listPanelElement.parentElement?.getBoundingClientRect();
173
+ if (!parentBounds) return;
174
+ const nextWidth = clampListPanelWidth(
175
+ Math.round(clientX - parentBounds.left),
176
+ );
177
+ setListPanelWidthPx(nextWidth);
178
+ }, []);
179
+
180
+ const onListResizerPointerDown = useCallback(
181
+ (event) => {
182
+ event.preventDefault();
183
+ setIsResizingListPanel(true);
184
+ resizeListPanelWithClientX(event.clientX);
185
+ },
186
+ [resizeListPanelWithClientX],
187
+ );
188
+
189
+ useEffect(() => {
190
+ if (!isResizingListPanel) return () => {};
191
+ const onPointerMove = (event) => resizeListPanelWithClientX(event.clientX);
192
+ const onPointerUp = () => setIsResizingListPanel(false);
193
+ window.addEventListener("pointermove", onPointerMove);
194
+ window.addEventListener("pointerup", onPointerUp);
195
+ const previousUserSelect = document.body.style.userSelect;
196
+ const previousCursor = document.body.style.cursor;
197
+ document.body.style.userSelect = "none";
198
+ document.body.style.cursor = "col-resize";
199
+ return () => {
200
+ window.removeEventListener("pointermove", onPointerMove);
201
+ window.removeEventListener("pointerup", onPointerUp);
202
+ document.body.style.userSelect = previousUserSelect;
203
+ document.body.style.cursor = previousCursor;
204
+ };
205
+ }, [isResizingListPanel, resizeListPanelWithClientX]);
206
+
207
+ const selectAllJobs = useCallback(() => {
208
+ onSetLocation("/cron");
209
+ }, [onSetLocation]);
210
+
211
+ const selectJob = useCallback(
212
+ (nextJobId) => {
213
+ onSetLocation(`/cron/${encodeURIComponent(String(nextJobId || ""))}`);
214
+ },
215
+ [onSetLocation],
216
+ );
217
+
218
+ const refreshAll = useCallback(() => {
219
+ jobsPoll.refresh();
220
+ statusPoll.refresh();
221
+ runsPoll.refresh();
222
+ usagePoll.refresh();
223
+ bulkUsagePoll.refresh();
224
+ bulkRunsPoll.refresh();
225
+ }, [
226
+ bulkRunsPoll.refresh,
227
+ bulkUsagePoll.refresh,
228
+ jobsPoll.refresh,
229
+ runsPoll.refresh,
230
+ statusPoll.refresh,
231
+ usagePoll.refresh,
232
+ ]);
233
+
234
+ const runSelectedJobNow = useCallback(async () => {
235
+ if (!selectedJobId || runningJob) return;
236
+ setRunningJob(true);
237
+ try {
238
+ await triggerCronJobRun(selectedJobId);
239
+ showToast("Cron run triggered", "success");
240
+ refreshAll();
241
+ } catch (error) {
242
+ showToast(error.message || "Could not run cron job", "error");
243
+ } finally {
244
+ setRunningJob(false);
245
+ }
246
+ }, [refreshAll, runningJob, selectedJobId]);
247
+
248
+ const setSelectedJobEnabled = useCallback(
249
+ async (enabled) => {
250
+ if (!selectedJobId || togglingJobEnabled) return;
251
+ setTogglingJobEnabled(true);
252
+ try {
253
+ await setCronJobEnabled(selectedJobId, enabled);
254
+ showToast(
255
+ enabled ? "Cron job enabled" : "Cron job disabled",
256
+ "success",
257
+ );
258
+ refreshAll();
259
+ } catch (error) {
260
+ showToast(error.message || "Could not update cron job", "error");
261
+ } finally {
262
+ setTogglingJobEnabled(false);
263
+ }
264
+ },
265
+ [refreshAll, selectedJobId, togglingJobEnabled],
266
+ );
267
+
268
+ const loadMoreRuns = useCallback(async () => {
269
+ if (!selectedJobId || !runHasMore || loadingMoreRuns) return;
270
+ setLoadingMoreRuns(true);
271
+ try {
272
+ const data = await fetchCronJobRuns(selectedJobId, {
273
+ limit: kRunsPageSize,
274
+ offset: runNextOffset,
275
+ status: runStatusFilter,
276
+ deliveryStatus: runDeliveryFilter,
277
+ sortDir: "desc",
278
+ });
279
+ const nextEntries = Array.isArray(data?.runs?.entries)
280
+ ? data.runs.entries
281
+ : [];
282
+ setRunEntries((currentValue) => [...currentValue, ...nextEntries]);
283
+ setRunHasMore(!!data?.runs?.hasMore);
284
+ setRunNextOffset(Number(data?.runs?.nextOffset || 0));
285
+ setRunTotal(Number(data?.runs?.total || 0));
286
+ } catch (error) {
287
+ showToast(error.message || "Could not load more runs", "error");
288
+ } finally {
289
+ setLoadingMoreRuns(false);
290
+ }
291
+ }, [
292
+ loadingMoreRuns,
293
+ runDeliveryFilter,
294
+ runHasMore,
295
+ runNextOffset,
296
+ runStatusFilter,
297
+ selectedJobId,
298
+ ]);
299
+
300
+ const savePrompt = useCallback(async () => {
301
+ if (!selectedJobId || savingPrompt) return;
302
+ if (promptValue === savedPromptValue) return;
303
+ setSavingPrompt(true);
304
+ try {
305
+ await updateCronJobPrompt(selectedJobId, promptValue);
306
+ setSavedPromptValue(promptValue);
307
+ showToast("Cron prompt updated", "success");
308
+ refreshAll();
309
+ } catch (error) {
310
+ showToast(error.message || "Could not update prompt", "error");
311
+ } finally {
312
+ setSavingPrompt(false);
313
+ }
314
+ }, [promptValue, refreshAll, savedPromptValue, savingPrompt, selectedJobId]);
315
+
316
+ return {
317
+ refs: {
318
+ listPanelRef,
319
+ },
320
+ state: {
321
+ jobs,
322
+ jobsError: jobsPoll.error,
323
+ status: statusPoll.data?.status || null,
324
+ statusError: statusPoll.error,
325
+ selectedRouteKey,
326
+ selectedJobId,
327
+ selectedJob,
328
+ listPanelWidthPx,
329
+ isResizingListPanel,
330
+ runEntries,
331
+ runHasMore,
332
+ runNextOffset,
333
+ runTotal,
334
+ runStatusFilter,
335
+ runDeliveryFilter,
336
+ runsError: runsPoll.error,
337
+ loadingMoreRuns,
338
+ usage: usagePoll.data?.usage || null,
339
+ usageError: usagePoll.error,
340
+ usageDays,
341
+ bulkUsageByJobId: bulkUsagePoll.data?.usage?.byJobId || {},
342
+ bulkUsageError: bulkUsagePoll.error,
343
+ bulkRunsByJobId: bulkRunsPoll.data?.runs?.byJobId || {},
344
+ bulkRunsError: bulkRunsPoll.error,
345
+ promptValue,
346
+ savedPromptValue,
347
+ savingPrompt,
348
+ runningJob,
349
+ togglingJobEnabled,
350
+ },
351
+ actions: {
352
+ setRunStatusFilter,
353
+ setRunDeliveryFilter,
354
+ setUsageDays,
355
+ setPromptValue,
356
+ savePrompt,
357
+ refreshAll,
358
+ loadMoreRuns,
359
+ runSelectedJobNow,
360
+ setSelectedJobEnabled,
361
+ selectAllJobs,
362
+ selectJob,
363
+ onListResizerPointerDown,
364
+ },
365
+ };
366
+ };
@@ -1,24 +1,18 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import htm from "https://esm.sh/htm";
3
+ import { SummaryStatCard } from "../summary-stat-card.js";
3
4
  import { buildDoctorPriorityCounts } from "./helpers.js";
4
5
 
5
6
  const html = htm.bind(h);
6
7
 
7
- const SummaryCard = ({ title = "", value = 0, toneClassName = "" }) => html`
8
- <div class="bg-surface border border-border rounded-xl p-4">
9
- <h3 class="card-label text-xs">${title}</h3>
10
- <div class=${`text-lg font-semibold mt-1 ${toneClassName}`}>${value}</div>
11
- </div>
12
- `;
13
-
14
8
  export const DoctorSummaryCards = ({ cards = [] }) => {
15
9
  const counts = buildDoctorPriorityCounts(cards);
16
10
  return html`
17
11
  <div class="grid grid-cols-1 md:grid-cols-4 gap-3">
18
- <${SummaryCard} title="Open Findings" value=${cards.length} />
19
- <${SummaryCard} title="P0" value=${counts.P0} toneClassName="text-red-400" />
20
- <${SummaryCard} title="P1" value=${counts.P1} toneClassName="text-yellow-400" />
21
- <${SummaryCard} title="P2" value=${counts.P2} toneClassName="text-gray-300" />
12
+ <${SummaryStatCard} title="Open Findings" value=${cards.length} />
13
+ <${SummaryStatCard} title="P0" value=${counts.P0} toneClassName="text-red-400" />
14
+ <${SummaryStatCard} title="P1" value=${counts.P1} toneClassName="text-yellow-400" />
15
+ <${SummaryStatCard} title="P2" value=${counts.P2} toneClassName="text-gray-300" />
22
16
  </div>
23
17
  `;
24
18
  };
@@ -47,14 +47,14 @@ const copyText = async (value) => {
47
47
  }
48
48
  };
49
49
 
50
- const kStepTitles = [
50
+ const kSetupStepTitles = [
51
51
  "Install + Authenticate gcloud",
52
52
  "Enable APIs",
53
53
  "Create Topic + IAM",
54
54
  "Create Push Subscription",
55
55
  "Build with your Agent",
56
56
  ];
57
- const kTotalSteps = kStepTitles.length;
57
+ const kTutorialStepTitles = kSetupStepTitles.slice(0, 3);
58
58
  const kNoSessionSelectedValue = kNoDestinationSessionValue;
59
59
 
60
60
  const renderCommandBlock = (command = "", onCopy = () => {}) => html`
@@ -136,6 +136,8 @@ export const GmailSetupWizard = ({
136
136
  String(clientConfig?.projectId || "").trim() ||
137
137
  "<project-id>";
138
138
  const hasExistingWebhookSetup = Boolean(clientConfig?.webhookExists);
139
+ const stepTitles = hasExistingWebhookSetup ? kTutorialStepTitles : kSetupStepTitles;
140
+ const totalSteps = stepTitles.length;
139
141
  const client =
140
142
  String(account?.client || clientConfig?.client || "default").trim() ||
141
143
  "default";
@@ -172,7 +174,7 @@ export const GmailSetupWizard = ({
172
174
  destination: selectedDestination,
173
175
  });
174
176
  setWatchEnabled(true);
175
- setStep((prev) => Math.min(prev + 1, kTotalSteps - 1));
177
+ setStep((prev) => Math.min(prev + 1, totalSteps - 1));
176
178
  } catch (err) {
177
179
  setLocalError(err.message || "Could not finish setup");
178
180
  }
@@ -196,7 +198,7 @@ export const GmailSetupWizard = ({
196
198
  }
197
199
  return;
198
200
  }
199
- setStep((prev) => Math.min(prev + 1, kTotalSteps - 1));
201
+ setStep((prev) => Math.min(prev + 1, totalSteps - 1));
200
202
  };
201
203
 
202
204
  const handleSendToAgent = async () => {
@@ -240,7 +242,7 @@ export const GmailSetupWizard = ({
240
242
  </button>
241
243
  <div class="text-xs text-gray-500">Gmail Pub / Sub Setup</div>
242
244
  <div class="flex items-center gap-1">
243
- ${kStepTitles.map(
245
+ ${stepTitles.map(
244
246
  (title, idx) => html`
245
247
  <div
246
248
  class=${`h-1 flex-1 rounded-full transition-colors ${idx <= step ? "bg-accent" : "bg-border"}`}
@@ -251,7 +253,7 @@ export const GmailSetupWizard = ({
251
253
  )}
252
254
  </div>
253
255
  <${PageHeader}
254
- title=${`Step ${step + 1} of ${kTotalSteps}: ${kStepTitles[step]}`}
256
+ title=${`Step ${step + 1} of ${totalSteps}: ${stepTitles[step]}`}
255
257
  actions=${null}
256
258
  />
257
259
  ${localError ? html`<div class="text-xs text-red-400">${localError}</div>` : null}
@@ -341,7 +343,7 @@ export const GmailSetupWizard = ({
341
343
  : null
342
344
  }
343
345
  ${
344
- !needsProjectId && step === 3
346
+ !hasExistingWebhookSetup && !needsProjectId && step === 3
345
347
  ? html`
346
348
  ${renderCommandBlock(commands?.createSubscription || "", () =>
347
349
  handleCopy(commands?.createSubscription || ""),
@@ -376,7 +378,7 @@ export const GmailSetupWizard = ({
376
378
  : null
377
379
  }
378
380
  ${
379
- step === 4
381
+ !hasExistingWebhookSetup && step === 4
380
382
  ? html`
381
383
  <div
382
384
  class="rounded-lg border border-border bg-black/20 p-3 space-y-3"
@@ -469,7 +471,18 @@ export const GmailSetupWizard = ({
469
471
  />`
470
472
  }
471
473
  ${
472
- step < kTotalSteps - 2
474
+ !hasExistingWebhookSetup && step === totalSteps - 2
475
+ ? html`<${ActionButton}
476
+ onClick=${handleFinish}
477
+ disabled=${false}
478
+ loading=${saving}
479
+ idleLabel="Enable watch"
480
+ loadingLabel="Enabling..."
481
+ tone="primary"
482
+ size="md"
483
+ className="w-full justify-center"
484
+ />`
485
+ : step < totalSteps - 1
473
486
  ? html`<${ActionButton}
474
487
  onClick=${handleNext}
475
488
  disabled=${saving || (needsProjectId && !canAdvance)}
@@ -478,27 +491,14 @@ export const GmailSetupWizard = ({
478
491
  size="md"
479
492
  className="w-full justify-center"
480
493
  />`
481
- : step === kTotalSteps - 2
482
- ? html`<${ActionButton}
483
- onClick=${hasExistingWebhookSetup ? handleNext : handleFinish}
484
- disabled=${false}
485
- loading=${saving}
486
- idleLabel=${hasExistingWebhookSetup ? "Next" : "Enable watch"}
487
- loadingLabel=${hasExistingWebhookSetup
488
- ? "Loading..."
489
- : "Enabling..."}
490
- tone="primary"
491
- size="md"
492
- className="w-full justify-center"
493
- />`
494
- : html`<${ActionButton}
495
- onClick=${onClose}
496
- disabled=${saving || sendingToAgent}
497
- idleLabel="Done"
498
- tone="secondary"
499
- size="md"
500
- className="w-full justify-center"
501
- />`
494
+ : html`<${ActionButton}
495
+ onClick=${onClose}
496
+ disabled=${saving || sendingToAgent}
497
+ idleLabel="Done"
498
+ tone="secondary"
499
+ size="md"
500
+ className="w-full justify-center"
501
+ />`
502
502
  }
503
503
  </div>
504
504
  </${ModalShell}>
@@ -373,7 +373,7 @@ export const Google = ({
373
373
  if (!account) return;
374
374
  const client = String(account.client || "default").trim() || "default";
375
375
  const clientConfig = clientConfigByClient.get(client);
376
- if (!clientConfig?.configured) {
376
+ if (!clientConfig?.configured || !clientConfig?.webhookExists) {
377
377
  openGmailSetupWizard(accountId);
378
378
  return;
379
379
  }
@@ -415,3 +415,16 @@ export const RestartLineIcon = ({ className = "" }) => html`
415
415
  />
416
416
  </svg>
417
417
  `;
418
+
419
+ export const ErrorWarningLineIcon = ({ className = "" }) => html`
420
+ <svg
421
+ class=${className}
422
+ viewBox="0 0 24 24"
423
+ fill="currentColor"
424
+ aria-hidden="true"
425
+ >
426
+ <path
427
+ d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z"
428
+ />
429
+ </svg>
430
+ `;
@@ -0,0 +1,33 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ const kPillBaseClassName =
7
+ "inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-medium transition-colors";
8
+ const kPillActiveClassName =
9
+ "border-cyan-500/40 bg-cyan-500/10 text-cyan-200 shadow-[0_0_0_1px_rgba(34,211,238,0.08)]";
10
+ const kPillInactiveClassName =
11
+ "border-border bg-black/20 text-gray-500 hover:border-gray-500 hover:text-gray-300";
12
+
13
+ export const PillTabs = ({
14
+ tabs = [],
15
+ activeTab = "",
16
+ onSelectTab = () => {},
17
+ className = "flex items-center gap-2",
18
+ } = {}) => html`
19
+ <div class=${className}>
20
+ ${tabs.map(
21
+ (tab) => html`
22
+ <button
23
+ key=${String(tab?.value || "")}
24
+ type="button"
25
+ class=${`${kPillBaseClassName} ${activeTab === tab?.value ? kPillActiveClassName : kPillInactiveClassName}`}
26
+ onclick=${() => onSelectTab(tab?.value)}
27
+ >
28
+ ${String(tab?.label || tab?.value || "")}
29
+ </button>
30
+ `,
31
+ )}
32
+ </div>
33
+ `;
@@ -0,0 +1,58 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ const kEnterDurationMs = 260;
8
+ const kExitDurationMs = 200;
9
+
10
+ /**
11
+ * Wrapper that pop-animates children in/out based on `visible`.
12
+ * Use for header save/cancel actions or any contextual action group.
13
+ *
14
+ * @param {boolean} props.visible Whether the actions should be shown.
15
+ * @param {string} [props.className] Extra classes on the container.
16
+ * @param {preact.ComponentChildren} props.children
17
+ */
18
+ export const PopActions = ({ visible = false, className = "", children }) => {
19
+ const [phase, setPhase] = useState(visible ? "visible" : "hidden");
20
+ const enterTimerRef = useRef(null);
21
+ const exitTimerRef = useRef(null);
22
+
23
+ useEffect(() => {
24
+ clearTimeout(enterTimerRef.current);
25
+ clearTimeout(exitTimerRef.current);
26
+ if (visible) {
27
+ if (phase !== "visible") {
28
+ setPhase("entering");
29
+ enterTimerRef.current = setTimeout(
30
+ () => setPhase("visible"),
31
+ kEnterDurationMs,
32
+ );
33
+ }
34
+ } else if (phase !== "hidden") {
35
+ setPhase("exiting");
36
+ exitTimerRef.current = setTimeout(() => setPhase("hidden"), kExitDurationMs);
37
+ }
38
+ return () => {
39
+ clearTimeout(enterTimerRef.current);
40
+ clearTimeout(exitTimerRef.current);
41
+ };
42
+ }, [visible, phase]);
43
+
44
+ const phaseClass =
45
+ phase === "entering"
46
+ ? "ac-pop-actions-in"
47
+ : phase === "exiting"
48
+ ? "ac-pop-actions-out"
49
+ : phase === "visible"
50
+ ? "ac-pop-actions-visible"
51
+ : "ac-pop-actions-hidden";
52
+
53
+ return html`
54
+ <div class=${`ac-pop-actions ${phaseClass} ${className}`.trim()}>
55
+ ${children}
56
+ </div>
57
+ `;
58
+ };
@@ -10,7 +10,9 @@ export const AgentsRoute = ({
10
10
  saving = false,
11
11
  agentsActions = {},
12
12
  selectedAgentId = "",
13
+ activeTab = "overview",
13
14
  onSelectAgent = () => {},
15
+ onSelectTab = () => {},
14
16
  onNavigateToBrowseFile = () => {},
15
17
  onSetLocation = () => {},
16
18
  }) => html`
@@ -20,7 +22,9 @@ export const AgentsRoute = ({
20
22
  saving=${saving}
21
23
  agentsActions=${agentsActions}
22
24
  selectedAgentId=${selectedAgentId}
25
+ activeTab=${activeTab}
23
26
  onSelectAgent=${onSelectAgent}
27
+ onSelectTab=${onSelectTab}
24
28
  onNavigateToBrowseFile=${onNavigateToBrowseFile}
25
29
  onSetLocation=${onSetLocation}
26
30
  />
@@ -0,0 +1,9 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { CronTab } from "../cron-tab/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const CronRoute = ({ jobId = "", onSetLocation = () => {} }) => html`
8
+ <${CronTab} jobId=${jobId} onSetLocation=${onSetLocation} />
9
+ `;
@@ -1,5 +1,6 @@
1
1
  export { AgentsRoute } from "./agents-route.js";
2
2
  export { BrowseRoute } from "./browse-route.js";
3
+ export { CronRoute } from "./cron-route.js";
3
4
  export { DoctorRoute } from "./doctor-route.js";
4
5
  export { EnvarsRoute } from "./envars-route.js";
5
6
  export { GeneralRoute } from "./general-route.js";