@chrysb/alphaclaw 0.6.2-beta.4 → 0.6.2-beta.6
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/assets/icons/slack.svg +17 -0
- package/lib/public/css/cron.css +91 -39
- package/lib/public/js/components/add-channel-menu.js +59 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +14 -38
- package/lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js +0 -6
- package/lib/public/js/components/agents-tab/create-channel-modal.js +185 -47
- package/lib/public/js/components/channels.js +15 -44
- package/lib/public/js/components/cron-tab/cron-calendar.js +287 -164
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +325 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +69 -56
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +20 -2
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/envars.js +4 -3
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
- package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
- package/lib/public/js/components/file-viewer/utils.js +1 -5
- package/lib/public/js/components/onboarding/pairing-utils.js +1 -0
- package/lib/public/js/components/onboarding/welcome-config.js +31 -1
- package/lib/public/js/components/onboarding/welcome-form-step.js +145 -67
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +89 -50
- package/lib/public/js/components/pairings.js +1 -1
- package/lib/public/js/components/welcome/index.js +1 -0
- package/lib/public/js/lib/channel-provider-availability.js +23 -0
- package/lib/server/agents/channels.js +110 -6
- package/lib/server/agents/shared.js +70 -1
- package/lib/server/constants.js +13 -0
- package/lib/server/gateway.js +28 -11
- package/lib/server/onboarding/openclaw.js +30 -0
- package/lib/server/onboarding/validation.js +1 -1
- package/lib/server/routes/pairings.js +2 -2
- package/lib/server/routes/system.js +9 -2
- package/lib/server/slack-api.js +38 -0
- package/lib/server/watchdog-notify.js +20 -3
- package/lib/server.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
+
import { Badge } from "../badge.js";
|
|
6
|
+
import { formatCost, formatTokenCount } from "./cron-helpers.js";
|
|
7
|
+
|
|
8
|
+
const html = htm.bind(h);
|
|
9
|
+
|
|
10
|
+
const kRange24h = "24h";
|
|
11
|
+
const kRange7d = "7d";
|
|
12
|
+
const kRange30d = "30d";
|
|
13
|
+
const kRangeOptions = [
|
|
14
|
+
{ label: "24h", value: kRange24h },
|
|
15
|
+
{ label: "7d", value: kRange7d },
|
|
16
|
+
{ label: "30d", value: kRange30d },
|
|
17
|
+
];
|
|
18
|
+
const kRangeWindowMsByValue = {
|
|
19
|
+
[kRange24h]: 24 * 60 * 60 * 1000,
|
|
20
|
+
[kRange7d]: 7 * 24 * 60 * 60 * 1000,
|
|
21
|
+
[kRange30d]: 30 * 24 * 60 * 60 * 1000,
|
|
22
|
+
};
|
|
23
|
+
const kTopListLimit = 3;
|
|
24
|
+
const kBadgeToneByInsight = {
|
|
25
|
+
"Token hungry": "warning",
|
|
26
|
+
"Potentially wasteful": "danger",
|
|
27
|
+
"Most expensive": "accent",
|
|
28
|
+
};
|
|
29
|
+
const kWastefulMinRuns = 10;
|
|
30
|
+
const kTokenHungryMinAvgTokensPerRun = 100000;
|
|
31
|
+
const kTokenHungryMinRuns = 3;
|
|
32
|
+
const formatRunCountLabel = (count = 0) => {
|
|
33
|
+
const safeCount = Number(count || 0);
|
|
34
|
+
const countLabel = formatTokenCount(safeCount);
|
|
35
|
+
return `${countLabel} ${safeCount === 1 ? "run" : "runs"}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getRunTotalTokens = (entry = {}) => {
|
|
39
|
+
const usage = entry?.usage || {};
|
|
40
|
+
const candidates = [
|
|
41
|
+
usage?.total_tokens,
|
|
42
|
+
usage?.totalTokens,
|
|
43
|
+
entry?.total_tokens,
|
|
44
|
+
entry?.totalTokens,
|
|
45
|
+
];
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
const numericValue = Number(candidate);
|
|
48
|
+
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getRunEstimatedCost = (entry = {}) => {
|
|
54
|
+
const usage = entry?.usage || {};
|
|
55
|
+
const candidates = [
|
|
56
|
+
entry?.estimatedCost,
|
|
57
|
+
entry?.estimated_cost,
|
|
58
|
+
usage?.estimatedCost,
|
|
59
|
+
usage?.estimated_cost,
|
|
60
|
+
usage?.totalCost,
|
|
61
|
+
usage?.total_cost,
|
|
62
|
+
usage?.costUsd,
|
|
63
|
+
usage?.cost,
|
|
64
|
+
];
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
const numericValue = Number(candidate);
|
|
67
|
+
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const readDeliveryMode = (job = null) =>
|
|
73
|
+
String(job?.delivery?.mode || job?.deliveryMode || "none")
|
|
74
|
+
.trim()
|
|
75
|
+
.toLowerCase();
|
|
76
|
+
|
|
77
|
+
const sortDescBy = (items = [], selectors = []) =>
|
|
78
|
+
[...items].sort((left, right) => {
|
|
79
|
+
for (const selector of selectors) {
|
|
80
|
+
const leftValue = Number(selector(left) || 0);
|
|
81
|
+
const rightValue = Number(selector(right) || 0);
|
|
82
|
+
if (leftValue === rightValue) continue;
|
|
83
|
+
return rightValue - leftValue;
|
|
84
|
+
}
|
|
85
|
+
return String(left?.jobName || "").localeCompare(
|
|
86
|
+
String(right?.jobName || ""),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const buildInsightMetrics = ({
|
|
91
|
+
jobs = [],
|
|
92
|
+
bulkRunsByJobId = {},
|
|
93
|
+
rangeValue = kRange7d,
|
|
94
|
+
}) => {
|
|
95
|
+
const nowMs = Date.now();
|
|
96
|
+
const windowMs = Number(
|
|
97
|
+
kRangeWindowMsByValue[rangeValue] || kRangeWindowMsByValue[kRange7d],
|
|
98
|
+
);
|
|
99
|
+
const cutoffMs = nowMs - windowMs;
|
|
100
|
+
const metricsByJobId = jobs.reduce((accumulator, job) => {
|
|
101
|
+
const jobId = String(job?.id || "");
|
|
102
|
+
if (!jobId) return accumulator;
|
|
103
|
+
accumulator[jobId] = {
|
|
104
|
+
jobId,
|
|
105
|
+
jobName: String(job?.name || jobId),
|
|
106
|
+
runCount: 0,
|
|
107
|
+
totalTokens: 0,
|
|
108
|
+
totalCost: 0,
|
|
109
|
+
hasCostData: false,
|
|
110
|
+
hasDelivery: readDeliveryMode(job) !== "none",
|
|
111
|
+
};
|
|
112
|
+
return accumulator;
|
|
113
|
+
}, {});
|
|
114
|
+
|
|
115
|
+
Object.entries(bulkRunsByJobId || {}).forEach(([jobIdValue, runResult]) => {
|
|
116
|
+
const jobId = String(jobIdValue || "");
|
|
117
|
+
if (!jobId) return;
|
|
118
|
+
if (!metricsByJobId[jobId]) {
|
|
119
|
+
metricsByJobId[jobId] = {
|
|
120
|
+
jobId,
|
|
121
|
+
jobName: jobId,
|
|
122
|
+
runCount: 0,
|
|
123
|
+
totalTokens: 0,
|
|
124
|
+
totalCost: 0,
|
|
125
|
+
hasCostData: false,
|
|
126
|
+
hasDelivery: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
|
|
130
|
+
entries.forEach((entry) => {
|
|
131
|
+
const timestampMs = Number(entry?.ts || 0);
|
|
132
|
+
if (
|
|
133
|
+
!Number.isFinite(timestampMs) ||
|
|
134
|
+
timestampMs < cutoffMs ||
|
|
135
|
+
timestampMs > nowMs
|
|
136
|
+
) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
metricsByJobId[jobId].runCount += 1;
|
|
140
|
+
metricsByJobId[jobId].totalTokens += Number(
|
|
141
|
+
getRunTotalTokens(entry) || 0,
|
|
142
|
+
);
|
|
143
|
+
const estimatedCost = getRunEstimatedCost(entry);
|
|
144
|
+
if (estimatedCost != null) {
|
|
145
|
+
metricsByJobId[jobId].hasCostData = true;
|
|
146
|
+
metricsByJobId[jobId].totalCost += Number(estimatedCost || 0);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return Object.values(metricsByJobId).map((entry) => ({
|
|
152
|
+
...entry,
|
|
153
|
+
avgTokensPerRun:
|
|
154
|
+
entry.runCount > 0 ? Math.round(entry.totalTokens / entry.runCount) : 0,
|
|
155
|
+
avgCostPerRun: entry.runCount > 0 ? entry.totalCost / entry.runCount : 0,
|
|
156
|
+
}));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const renderInsightRow = ({
|
|
160
|
+
title = "",
|
|
161
|
+
rows = [],
|
|
162
|
+
onSelectJob = () => {},
|
|
163
|
+
}) => {
|
|
164
|
+
const badgeTone = kBadgeToneByInsight[title] || "neutral";
|
|
165
|
+
const topRow = rows[0];
|
|
166
|
+
const overflowRows = rows.slice(1);
|
|
167
|
+
return html`
|
|
168
|
+
<div class="rounded-lg border border-border bg-black/20 px-3 py-2 space-y-1.5">
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="w-full text-left hover:brightness-110 transition"
|
|
172
|
+
onClick=${() => onSelectJob(topRow.jobId)}
|
|
173
|
+
>
|
|
174
|
+
<div class="flex items-start justify-between gap-3">
|
|
175
|
+
<div class="min-w-0">
|
|
176
|
+
<div class="text-sm text-gray-100 truncate">${topRow.jobName}</div>
|
|
177
|
+
<div class="text-xs text-gray-400 truncate mt-1">
|
|
178
|
+
${`${topRow.primaryLabel} · ${topRow.secondaryLabel}`}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="shrink-0 inline-flex items-center gap-1.5">
|
|
182
|
+
<${Badge} tone=${badgeTone}>${title}</${Badge}>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</button>
|
|
186
|
+
${
|
|
187
|
+
overflowRows.length > 0
|
|
188
|
+
? html`
|
|
189
|
+
<details class="group">
|
|
190
|
+
<summary
|
|
191
|
+
class="list-none cursor-pointer text-[11px] text-gray-500 hover:text-gray-300"
|
|
192
|
+
>
|
|
193
|
+
Show more
|
|
194
|
+
</summary>
|
|
195
|
+
<div class="mt-1.5 divide-y divide-border">
|
|
196
|
+
${overflowRows.map(
|
|
197
|
+
(row, index) => html`
|
|
198
|
+
<button
|
|
199
|
+
key=${`${title}:${row.jobId}`}
|
|
200
|
+
type="button"
|
|
201
|
+
class="w-full text-left py-2 hover:brightness-110 transition"
|
|
202
|
+
onClick=${() => onSelectJob(row.jobId)}
|
|
203
|
+
>
|
|
204
|
+
<div class="flex items-start justify-between gap-3">
|
|
205
|
+
<div class="min-w-0">
|
|
206
|
+
<div class="text-sm text-gray-200 truncate">
|
|
207
|
+
${row.jobName}
|
|
208
|
+
</div>
|
|
209
|
+
<div
|
|
210
|
+
class="text-[11px] text-gray-500 truncate mt-1"
|
|
211
|
+
>
|
|
212
|
+
${`${row.primaryLabel} · ${row.secondaryLabel}`}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div
|
|
216
|
+
class="text-[11px] uppercase tracking-wide text-gray-500"
|
|
217
|
+
>
|
|
218
|
+
#${index + 2}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</button>
|
|
222
|
+
`,
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
</details>
|
|
226
|
+
`
|
|
227
|
+
: null
|
|
228
|
+
}
|
|
229
|
+
</div>
|
|
230
|
+
`;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const CronInsightsPanel = ({
|
|
234
|
+
jobs = [],
|
|
235
|
+
bulkRunsByJobId = {},
|
|
236
|
+
onSelectJob = () => {},
|
|
237
|
+
}) => {
|
|
238
|
+
const [rangeValue, setRangeValue] = useState(kRange7d);
|
|
239
|
+
const metrics = useMemo(
|
|
240
|
+
() => buildInsightMetrics({ jobs, bulkRunsByJobId, rangeValue }),
|
|
241
|
+
[bulkRunsByJobId, jobs, rangeValue],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const tokenHungryRows = useMemo(
|
|
245
|
+
() =>
|
|
246
|
+
sortDescBy(
|
|
247
|
+
metrics.filter(
|
|
248
|
+
(entry) =>
|
|
249
|
+
entry.runCount >= kTokenHungryMinRuns &&
|
|
250
|
+
entry.avgTokensPerRun >= kTokenHungryMinAvgTokensPerRun,
|
|
251
|
+
),
|
|
252
|
+
[(entry) => entry.avgTokensPerRun, (entry) => entry.totalTokens],
|
|
253
|
+
)
|
|
254
|
+
.slice(0, kTopListLimit)
|
|
255
|
+
.map((entry) => ({
|
|
256
|
+
...entry,
|
|
257
|
+
primaryLabel: `${formatTokenCount(entry.avgTokensPerRun)} avg tokens/run`,
|
|
258
|
+
secondaryLabel: `${formatTokenCount(entry.totalTokens)} total tokens · ${formatRunCountLabel(entry.runCount)}`,
|
|
259
|
+
})),
|
|
260
|
+
[metrics],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const wastefulRows = useMemo(
|
|
264
|
+
() =>
|
|
265
|
+
sortDescBy(
|
|
266
|
+
metrics.filter(
|
|
267
|
+
(entry) =>
|
|
268
|
+
entry.runCount >= kWastefulMinRuns &&
|
|
269
|
+
entry.hasDelivery === false &&
|
|
270
|
+
(entry.totalTokens > 0 || entry.totalCost > 0),
|
|
271
|
+
),
|
|
272
|
+
[(entry) => entry.totalTokens, (entry) => entry.runCount],
|
|
273
|
+
)
|
|
274
|
+
.slice(0, kTopListLimit)
|
|
275
|
+
.map((entry) => ({
|
|
276
|
+
...entry,
|
|
277
|
+
primaryLabel: `${formatRunCountLabel(entry.runCount)} with no delivery`,
|
|
278
|
+
secondaryLabel: `${formatTokenCount(entry.totalTokens)} total tokens${entry.hasCostData ? ` · ${formatCost(entry.totalCost)}` : ""}`,
|
|
279
|
+
})),
|
|
280
|
+
[metrics],
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const expensiveRows = useMemo(
|
|
284
|
+
() =>
|
|
285
|
+
sortDescBy(
|
|
286
|
+
metrics.filter((entry) => entry.runCount > 0 && entry.totalCost > 0),
|
|
287
|
+
[(entry) => entry.totalCost, (entry) => entry.avgCostPerRun],
|
|
288
|
+
)
|
|
289
|
+
.slice(0, kTopListLimit)
|
|
290
|
+
.map((entry) => ({
|
|
291
|
+
...entry,
|
|
292
|
+
primaryLabel: `${formatCost(entry.totalCost)} total estimated cost`,
|
|
293
|
+
secondaryLabel: `${formatCost(entry.avgCostPerRun)} avg/run · ${formatRunCountLabel(entry.runCount)}`,
|
|
294
|
+
})),
|
|
295
|
+
[metrics],
|
|
296
|
+
);
|
|
297
|
+
const insightRows = [
|
|
298
|
+
{ title: "Token hungry", rows: tokenHungryRows },
|
|
299
|
+
{ title: "Potentially wasteful", rows: wastefulRows },
|
|
300
|
+
{ title: "Most expensive", rows: expensiveRows },
|
|
301
|
+
].filter((group) => Array.isArray(group.rows) && group.rows.length > 0);
|
|
302
|
+
if (insightRows.length === 0) return null;
|
|
303
|
+
|
|
304
|
+
return html`
|
|
305
|
+
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
306
|
+
<div class="flex items-center justify-between gap-2">
|
|
307
|
+
<h3 class="card-label cron-calendar-title">Insights</h3>
|
|
308
|
+
<${SegmentedControl}
|
|
309
|
+
options=${kRangeOptions}
|
|
310
|
+
value=${rangeValue}
|
|
311
|
+
onChange=${setRangeValue}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div class="grid grid-cols-1 gap-2">
|
|
316
|
+
${insightRows.map((group) =>
|
|
317
|
+
renderInsightRow({
|
|
318
|
+
title: group.title,
|
|
319
|
+
rows: group.rows,
|
|
320
|
+
onSelectJob,
|
|
321
|
+
}))}
|
|
322
|
+
</div>
|
|
323
|
+
</section>
|
|
324
|
+
`;
|
|
325
|
+
};
|