@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,425 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { ActionButton } from "../action-button.js";
5
+ import { SegmentedControl } from "../segmented-control.js";
6
+ import { ToggleSwitch } from "../toggle-switch.js";
7
+ import { EditorSurface } from "../file-viewer/editor-surface.js";
8
+ import { countTextLines, shouldUseSimpleEditorMode } from "../file-viewer/utils.js";
9
+ import {
10
+ kLargeFileSimpleEditorCharThreshold,
11
+ kLargeFileSimpleEditorLineThreshold,
12
+ } from "../file-viewer/constants.js";
13
+ import { highlightEditorLines } from "../../lib/syntax-highlighters/index.js";
14
+ import { formatDurationCompactMs, formatLocaleDateTimeWithTodayTime } from "../../lib/format.js";
15
+ import {
16
+ formatCronScheduleLabel,
17
+ formatNextRunRelativeMs,
18
+ formatTokenCount,
19
+ } from "./cron-helpers.js";
20
+ import { CronJobUsage } from "./cron-job-usage.js";
21
+ import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
22
+
23
+ const html = htm.bind(h);
24
+ const kCronPromptEditorHeightUiSettingKey = "cronPromptEditorHeightPx";
25
+ const kCronPromptEditorDefaultHeightPx = 280;
26
+ const kCronPromptEditorMinHeightPx = 180;
27
+ const clampPromptEditorHeight = (value) => {
28
+ const parsed = Number(value);
29
+ const normalized = Number.isFinite(parsed)
30
+ ? Math.round(parsed)
31
+ : kCronPromptEditorDefaultHeightPx;
32
+ return Math.max(kCronPromptEditorMinHeightPx, normalized);
33
+ };
34
+ const readCssHeightPx = (element) => {
35
+ if (!element) return 0;
36
+ const computedHeight = Number.parseFloat(window.getComputedStyle(element).height || "0");
37
+ return Number.isFinite(computedHeight) ? computedHeight : 0;
38
+ };
39
+
40
+ const PromptEditor = ({
41
+ promptValue = "",
42
+ savedPromptValue = "",
43
+ onChangePrompt = () => {},
44
+ onSavePrompt = () => {},
45
+ savingPrompt = false,
46
+ }) => {
47
+ const promptEditorShellRef = useRef(null);
48
+ const editorTextareaRef = useRef(null);
49
+ const editorLineNumbersRef = useRef(null);
50
+ const editorLineNumberRowRefs = useRef([]);
51
+ const editorHighlightRef = useRef(null);
52
+ const editorHighlightLineRefs = useRef([]);
53
+ const [promptEditorHeightPx, setPromptEditorHeightPx] = useState(() => {
54
+ const settings = readUiSettings();
55
+ return clampPromptEditorHeight(settings?.[kCronPromptEditorHeightUiSettingKey]);
56
+ });
57
+
58
+ const lineCount = countTextLines(promptValue);
59
+ const editorLineNumbers = useMemo(
60
+ () => Array.from({ length: lineCount }, (_, index) => index + 1),
61
+ [lineCount],
62
+ );
63
+ const shouldUseHighlightedEditor = !shouldUseSimpleEditorMode({
64
+ contentLength: promptValue.length,
65
+ lineCount,
66
+ charThreshold: kLargeFileSimpleEditorCharThreshold,
67
+ lineThreshold: kLargeFileSimpleEditorLineThreshold,
68
+ });
69
+ const highlightedEditorLines = useMemo(
70
+ () =>
71
+ shouldUseHighlightedEditor
72
+ ? highlightEditorLines(promptValue, "markdown")
73
+ : [],
74
+ [promptValue, shouldUseHighlightedEditor],
75
+ );
76
+ const isDirty = promptValue !== savedPromptValue;
77
+
78
+ const handleEditorScroll = (event) => {
79
+ const scrollTop = event.currentTarget.scrollTop;
80
+ if (editorLineNumbersRef.current) editorLineNumbersRef.current.scrollTop = scrollTop;
81
+ if (editorHighlightRef.current) editorHighlightRef.current.scrollTop = scrollTop;
82
+ };
83
+
84
+ const handleEditorKeyDown = (event) => {
85
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
86
+ event.preventDefault();
87
+ onSavePrompt();
88
+ }
89
+ if (event.key === "Tab") {
90
+ event.preventDefault();
91
+ const textarea = editorTextareaRef.current;
92
+ if (!textarea) return;
93
+ const start = textarea.selectionStart;
94
+ const end = textarea.selectionEnd;
95
+ const nextValue = `${promptValue.slice(0, start)} ${promptValue.slice(end)}`;
96
+ onChangePrompt(nextValue);
97
+ window.requestAnimationFrame(() => {
98
+ textarea.selectionStart = start + 2;
99
+ textarea.selectionEnd = start + 2;
100
+ });
101
+ }
102
+ };
103
+
104
+ useEffect(() => {
105
+ const shellElement = promptEditorShellRef.current;
106
+ if (!shellElement || typeof ResizeObserver === "undefined") return () => {};
107
+
108
+ let saveTimer = null;
109
+ const observer = new ResizeObserver((entries) => {
110
+ const entry = entries?.[0];
111
+ const nextHeight = clampPromptEditorHeight(readCssHeightPx(entry?.target));
112
+ setPromptEditorHeightPx((currentValue) =>
113
+ Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue
114
+ );
115
+ if (saveTimer) window.clearTimeout(saveTimer);
116
+ saveTimer = window.setTimeout(() => {
117
+ const settings = readUiSettings();
118
+ settings[kCronPromptEditorHeightUiSettingKey] = nextHeight;
119
+ writeUiSettings(settings);
120
+ }, 120);
121
+ });
122
+ observer.observe(shellElement);
123
+ return () => {
124
+ observer.disconnect();
125
+ if (saveTimer) window.clearTimeout(saveTimer);
126
+ };
127
+ }, []);
128
+
129
+ return html`
130
+ <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
131
+ <div class="flex items-center justify-between gap-2">
132
+ <h3 class="card-label inline-flex items-center gap-1.5">
133
+ Prompt
134
+ ${isDirty ? html`<span class="file-viewer-dirty-dot"></span>` : null}
135
+ </h3>
136
+ <div class="flex items-center gap-2">
137
+ <${ActionButton}
138
+ onClick=${onSavePrompt}
139
+ disabled=${!isDirty}
140
+ loading=${savingPrompt}
141
+ tone="primary"
142
+ size="sm"
143
+ idleLabel="Save"
144
+ loadingLabel="Saving..."
145
+ />
146
+ </div>
147
+ </div>
148
+ <div
149
+ class="cron-prompt-editor-shell"
150
+ ref=${promptEditorShellRef}
151
+ style=${{ height: `${promptEditorHeightPx}px` }}
152
+ >
153
+ <${EditorSurface}
154
+ editorShellClassName="file-viewer-editor-shell"
155
+ editorLineNumbers=${editorLineNumbers}
156
+ editorLineNumbersRef=${editorLineNumbersRef}
157
+ editorLineNumberRowRefs=${editorLineNumberRowRefs}
158
+ shouldUseHighlightedEditor=${shouldUseHighlightedEditor}
159
+ highlightedEditorLines=${highlightedEditorLines}
160
+ editorHighlightRef=${editorHighlightRef}
161
+ editorHighlightLineRefs=${editorHighlightLineRefs}
162
+ editorTextareaRef=${editorTextareaRef}
163
+ renderContent=${promptValue}
164
+ handleContentInput=${(event) => onChangePrompt(event.target.value)}
165
+ handleEditorKeyDown=${handleEditorKeyDown}
166
+ handleEditorScroll=${handleEditorScroll}
167
+ handleEditorSelectionChange=${() => {}}
168
+ isEditBlocked=${false}
169
+ isPreviewOnly=${false}
170
+ />
171
+ </div>
172
+ </section>
173
+ `;
174
+ };
175
+
176
+ const runStatusClassName = (status) => {
177
+ const normalized = String(status || "").trim().toLowerCase();
178
+ if (normalized === "ok") return "text-green-300";
179
+ if (normalized === "error") return "text-red-300";
180
+ if (normalized === "skipped") return "text-yellow-300";
181
+ return "text-gray-400";
182
+ };
183
+
184
+ const runDeliveryLabel = (run) => String(run?.deliveryStatus || "not-requested");
185
+ const kMetaCardClassName = "ac-surface-inset rounded-lg p-2.5 space-y-1.5";
186
+ const kRunStatusFilterOptions = [
187
+ { label: "all", value: "all" },
188
+ { label: "ok", value: "ok" },
189
+ { label: "error", value: "error" },
190
+ { label: "skipped", value: "skipped" },
191
+ ];
192
+ const kRunDeliveryFilterOptions = [
193
+ { label: "all", value: "all" },
194
+ { label: "delivered", value: "delivered" },
195
+ { label: "not-delivered", value: "not-delivered" },
196
+ ];
197
+ const isSameCalendarDay = (leftDate, rightDate) =>
198
+ leftDate.getFullYear() === rightDate.getFullYear() &&
199
+ leftDate.getMonth() === rightDate.getMonth() &&
200
+ leftDate.getDate() === rightDate.getDate();
201
+
202
+ const formatCompactMeridiemTime = (dateValue) =>
203
+ dateValue
204
+ .toLocaleTimeString([], {
205
+ hour: "numeric",
206
+ minute: "2-digit",
207
+ })
208
+ .replace(/\s*([AP])M$/i, (_, marker) => `${String(marker || "").toLowerCase()}m`)
209
+ .replace(/\s+/g, "");
210
+
211
+ const formatNextRunAbsolute = (value) => {
212
+ const timestamp = Number(value || 0);
213
+ if (!Number.isFinite(timestamp) || timestamp <= 0) return "—";
214
+ const dateValue = new Date(timestamp);
215
+ if (Number.isNaN(dateValue.getTime())) return "—";
216
+ const nowValue = new Date();
217
+ const tomorrowValue = new Date(nowValue);
218
+ tomorrowValue.setDate(nowValue.getDate() + 1);
219
+ const isToday = isSameCalendarDay(dateValue, nowValue);
220
+ const isTomorrow = isSameCalendarDay(dateValue, tomorrowValue);
221
+ const compactTime = formatCompactMeridiemTime(dateValue);
222
+ if (isToday) return compactTime;
223
+ if (isTomorrow) return `Tomorrow ${compactTime}`;
224
+ return `${dateValue.toLocaleDateString()} ${compactTime}`;
225
+ };
226
+
227
+ export const CronJobDetail = ({
228
+ job = null,
229
+ runEntries = [],
230
+ runTotal = 0,
231
+ runHasMore = false,
232
+ loadingMoreRuns = false,
233
+ runStatusFilter = "all",
234
+ runDeliveryFilter = "all",
235
+ onSetRunStatusFilter = () => {},
236
+ onSetRunDeliveryFilter = () => {},
237
+ onLoadMoreRuns = () => {},
238
+ onRunNow = () => {},
239
+ runningJob = false,
240
+ onToggleEnabled = () => {},
241
+ togglingJobEnabled = false,
242
+ usage = null,
243
+ usageDays = 30,
244
+ onSetUsageDays = () => {},
245
+ promptValue = "",
246
+ savedPromptValue = "",
247
+ onChangePrompt = () => {},
248
+ onSavePrompt = () => {},
249
+ savingPrompt = false,
250
+ }) => {
251
+ if (!job) {
252
+ return html`
253
+ <div class="h-full flex items-center justify-center text-sm text-gray-500">
254
+ Select a cron job to view details.
255
+ </div>
256
+ `;
257
+ }
258
+
259
+ return html`
260
+ <div class="cron-detail-scroll">
261
+ <div class="cron-detail-content">
262
+ <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
263
+ <div class="flex items-center justify-between gap-3">
264
+ <div>
265
+ <h2 class="font-semibold text-base text-gray-100">${job.name || job.id}</h2>
266
+ <div class="text-xs text-gray-500 mt-1">ID: <code>${job.id}</code></div>
267
+ </div>
268
+ <div class="flex items-center gap-2">
269
+ <${ToggleSwitch}
270
+ checked=${job.enabled !== false}
271
+ disabled=${togglingJobEnabled}
272
+ onChange=${onToggleEnabled}
273
+ label=${job.enabled === false ? "Disabled" : "Enabled"}
274
+ />
275
+ <${ActionButton}
276
+ onClick=${onRunNow}
277
+ loading=${runningJob}
278
+ tone="secondary"
279
+ size="sm"
280
+ idleLabel="Run Now"
281
+ loadingLabel="Running..."
282
+ />
283
+ </div>
284
+ </div>
285
+ <div class="grid grid-cols-2 gap-2 text-xs">
286
+ <div class=${kMetaCardClassName}>
287
+ <div class="text-gray-500">Schedule</div>
288
+ <div class="text-gray-300 font-mono">
289
+ ${formatCronScheduleLabel(job.schedule, {
290
+ includeTimeZoneWhenDifferent: true,
291
+ })}
292
+ </div>
293
+ </div>
294
+ <div class=${kMetaCardClassName}>
295
+ <div class="text-gray-500">Next run</div>
296
+ <div class="text-gray-300 font-mono">
297
+ ${formatNextRunAbsolute(job?.state?.nextRunAtMs)}
298
+ <span class="text-gray-500">
299
+ ${` (${formatNextRunRelativeMs(job?.state?.nextRunAtMs)})`}
300
+ </span>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ <div class="grid grid-cols-3 gap-2 text-xs">
305
+ <div class=${kMetaCardClassName}>
306
+ <div class="text-gray-500">Session target</div>
307
+ <div class="text-gray-300 font-mono">${job.sessionTarget || "main"}</div>
308
+ </div>
309
+ <div class=${kMetaCardClassName}>
310
+ <div class="text-gray-500">Wake mode</div>
311
+ <div class="text-gray-300 font-mono">${job.wakeMode || "now"}</div>
312
+ </div>
313
+ <div class=${kMetaCardClassName}>
314
+ <div class="text-gray-500">Delivery</div>
315
+ <div class="text-gray-300 font-mono">
316
+ ${String(job?.delivery?.mode || "none")}
317
+ ${job?.delivery?.channel
318
+ ? html`- ${job.delivery.channel}${job?.delivery?.to
319
+ ? `:${job.delivery.to}`
320
+ : ""}`
321
+ : ""}
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </section>
326
+
327
+ <${PromptEditor}
328
+ promptValue=${promptValue}
329
+ savedPromptValue=${savedPromptValue}
330
+ onChangePrompt=${onChangePrompt}
331
+ onSavePrompt=${onSavePrompt}
332
+ savingPrompt=${savingPrompt}
333
+ />
334
+
335
+ <${CronJobUsage}
336
+ usage=${usage}
337
+ usageDays=${usageDays}
338
+ onSetUsageDays=${onSetUsageDays}
339
+ />
340
+
341
+ <section class="bg-surface border border-border rounded-xl p-4 space-y-3">
342
+ <div class="flex items-center justify-between gap-2">
343
+ <h3 class="card-label card-label-bright">Run history</h3>
344
+ <div class="text-xs text-gray-500">${formatTokenCount(runTotal)} entries</div>
345
+ </div>
346
+ <div class="flex items-center gap-2">
347
+ <${SegmentedControl}
348
+ options=${kRunStatusFilterOptions}
349
+ value=${runStatusFilter}
350
+ onChange=${onSetRunStatusFilter}
351
+ />
352
+ <${SegmentedControl}
353
+ options=${kRunDeliveryFilterOptions}
354
+ value=${runDeliveryFilter}
355
+ onChange=${onSetRunDeliveryFilter}
356
+ />
357
+ </div>
358
+
359
+ ${runEntries.length === 0
360
+ ? html`<div class="text-sm text-gray-500">No runs found.</div>`
361
+ : html`
362
+ <div class="ac-history-list">
363
+ ${runEntries.map(
364
+ (entry) => html`
365
+ <details key=${`${entry.ts}:${entry.sessionKey || ""}`} class="ac-history-item">
366
+ <summary class="ac-history-summary">
367
+ <div class="ac-history-summary-row">
368
+ <span class="inline-flex items-center gap-2 min-w-0">
369
+ <span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
370
+ <span class="truncate text-xs text-gray-300">
371
+ ${formatLocaleDateTimeWithTodayTime(entry.ts, {
372
+ fallback: "—",
373
+ valueIsEpochMs: true,
374
+ })}
375
+ </span>
376
+ </span>
377
+ <span class="inline-flex items-center gap-3 shrink-0 text-xs">
378
+ <span class=${runStatusClassName(entry.status)}>${entry.status || "unknown"}</span>
379
+ <span class="text-gray-400">${formatDurationCompactMs(entry.durationMs)}</span>
380
+ <span class="text-gray-400">
381
+ ${formatTokenCount(entry?.usage?.total_tokens || 0)} tk
382
+ </span>
383
+ <span class="text-gray-500">${runDeliveryLabel(entry)}</span>
384
+ </span>
385
+ </div>
386
+ </summary>
387
+ <div class="ac-history-body space-y-2 text-xs">
388
+ ${entry.summary
389
+ ? html`<div><span class="text-gray-500">Summary:</span> ${entry.summary}</div>`
390
+ : null}
391
+ ${entry.error
392
+ ? html`<div class="text-red-300"><span class="text-gray-500">Error:</span> ${entry.error}</div>`
393
+ : null}
394
+ <div class="text-gray-500">
395
+ Model: <span class="text-gray-300 font-mono">${entry.model || "—"}</span>
396
+ ${entry.sessionKey
397
+ ? html` | Session:
398
+ <span class="text-gray-300 font-mono">${entry.sessionKey}</span>`
399
+ : null}
400
+ </div>
401
+ </div>
402
+ </details>
403
+ `,
404
+ )}
405
+ </div>
406
+ `}
407
+ ${runHasMore
408
+ ? html`
409
+ <div class="pt-2">
410
+ <${ActionButton}
411
+ onClick=${onLoadMoreRuns}
412
+ loading=${loadingMoreRuns}
413
+ tone="secondary"
414
+ size="sm"
415
+ idleLabel="Load More"
416
+ loadingLabel="Loading..."
417
+ />
418
+ </div>
419
+ `
420
+ : null}
421
+ </section>
422
+ </div>
423
+ </div>
424
+ `;
425
+ };