@chrysb/alphaclaw 0.6.1 → 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.
- package/lib/public/css/agents.css +1 -1
- package/lib/public/css/cron.css +535 -0
- package/lib/public/css/theme.css +72 -0
- package/lib/public/js/app.js +45 -10
- package/lib/public/js/components/action-button.js +26 -20
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
- package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
- package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
- package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
- package/lib/public/js/components/agents-tab/index.js +4 -0
- package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
- package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
- package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
- package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
- package/lib/public/js/components/cron-tab/index.js +100 -0
- package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
- package/lib/public/js/components/doctor/summary-cards.js +5 -11
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/pill-tabs.js +33 -0
- package/lib/public/js/components/pop-actions.js +58 -0
- package/lib/public/js/components/routes/agents-route.js +4 -0
- package/lib/public/js/components/routes/cron-route.js +9 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/segmented-control.js +15 -9
- package/lib/public/js/components/summary-stat-card.js +17 -0
- package/lib/public/js/components/tooltip.js +50 -4
- package/lib/public/js/components/watchdog-tab.js +46 -1
- package/lib/public/js/lib/api.js +94 -0
- package/lib/public/js/lib/app-navigation.js +2 -0
- package/lib/public/js/lib/storage-keys.js +1 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/agents/agents.js +15 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/cost-utils.js +312 -0
- package/lib/server/cron-service.js +461 -0
- package/lib/server/db/usage/index.js +100 -1
- package/lib/server/db/usage/pricing.js +1 -83
- package/lib/server/db/usage/sessions.js +4 -1
- package/lib/server/db/usage/shared.js +2 -1
- package/lib/server/db/usage/summary.js +5 -1
- package/lib/server/onboarding/index.js +39 -5
- package/lib/server/onboarding/openclaw.js +25 -19
- package/lib/server/onboarding/validation.js +28 -0
- package/lib/server/routes/cron.js +148 -0
- package/lib/server.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,599 @@
|
|
|
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 {
|
|
5
|
+
buildCronOptimizationWarnings,
|
|
6
|
+
formatCost,
|
|
7
|
+
formatRelativeMs,
|
|
8
|
+
formatTokenCount,
|
|
9
|
+
getNextScheduledRunAcrossJobs,
|
|
10
|
+
} from "./cron-helpers.js";
|
|
11
|
+
import { CronCalendar } from "./cron-calendar.js";
|
|
12
|
+
import { CronRunsTrendCard } from "./cron-runs-trend-card.js";
|
|
13
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
14
|
+
import { SummaryStatCard } from "../summary-stat-card.js";
|
|
15
|
+
import { ErrorWarningLineIcon } from "../icons.js";
|
|
16
|
+
import {
|
|
17
|
+
formatDurationCompactMs,
|
|
18
|
+
formatLocaleDateTimeWithTodayTime,
|
|
19
|
+
} from "../../lib/format.js";
|
|
20
|
+
|
|
21
|
+
const html = htm.bind(h);
|
|
22
|
+
const kRecentRunFetchLimit = 100;
|
|
23
|
+
const kRecentRunRowsLimit = 20;
|
|
24
|
+
const kRecentRunCollapseThreshold = 5;
|
|
25
|
+
const kTrendRange7d = "7d";
|
|
26
|
+
const kTrendRange30d = "30d";
|
|
27
|
+
const kTrendQueryStartKey = "trendStart";
|
|
28
|
+
const kTrendQueryEndKey = "trendEnd";
|
|
29
|
+
const kTrendQueryRangeKey = "trendRange";
|
|
30
|
+
const kTrendQueryLabelKey = "trendLabel";
|
|
31
|
+
|
|
32
|
+
const kRunStatusFilterOptions = [
|
|
33
|
+
{ label: "all", value: "all" },
|
|
34
|
+
{ label: "ok", value: "ok" },
|
|
35
|
+
{ label: "error", value: "error" },
|
|
36
|
+
{ label: "skipped", value: "skipped" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const warningClassName = (tone) => {
|
|
40
|
+
if (tone === "error") return "border-red-900 bg-red-950/30 text-red-200";
|
|
41
|
+
if (tone === "warning")
|
|
42
|
+
return "border-yellow-900 bg-yellow-950/30 text-yellow-100";
|
|
43
|
+
return "border-border bg-black/20 text-gray-200";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const formatWarningsAttentionText = (warnings = []) => {
|
|
47
|
+
const errorCount = warnings.filter(
|
|
48
|
+
(warning) => warning?.tone === "error",
|
|
49
|
+
).length;
|
|
50
|
+
const warningCount = warnings.filter(
|
|
51
|
+
(warning) => warning?.tone === "warning",
|
|
52
|
+
).length;
|
|
53
|
+
const totalCount = errorCount + warningCount;
|
|
54
|
+
if (totalCount <= 0) return "No warnings currently need your attention";
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (errorCount > 0)
|
|
57
|
+
parts.push(`${errorCount} error${errorCount === 1 ? "" : "s"}`);
|
|
58
|
+
if (warningCount > 0)
|
|
59
|
+
parts.push(`${warningCount} warning${warningCount === 1 ? "" : "s"}`);
|
|
60
|
+
return `${parts.join(" and ")} may need your attention`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const runStatusClassName = (status = "") => {
|
|
64
|
+
const normalized = String(status || "")
|
|
65
|
+
.trim()
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
if (normalized === "ok") return "text-green-300";
|
|
68
|
+
if (normalized === "error") return "text-red-300";
|
|
69
|
+
if (normalized === "skipped") return "text-yellow-300";
|
|
70
|
+
return "text-gray-400";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const formatRecentRunTimestamp = (timestampMs) =>
|
|
74
|
+
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
75
|
+
fallback: "—",
|
|
76
|
+
valueIsEpochMs: true,
|
|
77
|
+
}).replace(
|
|
78
|
+
/\s([AP])M\b/g,
|
|
79
|
+
(_, marker) => `${String(marker || "").toLowerCase()}m`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const getRunEstimatedCost = (runEntry = {}) => {
|
|
83
|
+
const usage = runEntry?.usage || {};
|
|
84
|
+
const candidates = [
|
|
85
|
+
usage?.estimatedCost,
|
|
86
|
+
usage?.estimated_cost,
|
|
87
|
+
usage?.totalCost,
|
|
88
|
+
usage?.total_cost,
|
|
89
|
+
usage?.costUsd,
|
|
90
|
+
usage?.cost,
|
|
91
|
+
runEntry?.estimatedCost,
|
|
92
|
+
runEntry?.estimated_cost,
|
|
93
|
+
runEntry?.totalCost,
|
|
94
|
+
runEntry?.total_cost,
|
|
95
|
+
runEntry?.costUsd,
|
|
96
|
+
runEntry?.cost,
|
|
97
|
+
];
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
const numericValue = Number(candidate);
|
|
100
|
+
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const flattenRecentRuns = ({ bulkRunsByJobId = {}, jobs = [] } = {}) => {
|
|
106
|
+
const jobNameById = jobs.reduce((accumulator, job) => {
|
|
107
|
+
const jobId = String(job?.id || "");
|
|
108
|
+
if (!jobId) return accumulator;
|
|
109
|
+
accumulator[jobId] = String(job?.name || jobId);
|
|
110
|
+
return accumulator;
|
|
111
|
+
}, {});
|
|
112
|
+
return Object.entries(bulkRunsByJobId || {})
|
|
113
|
+
.flatMap(([jobId, runResult]) => {
|
|
114
|
+
const entries = Array.isArray(runResult?.entries)
|
|
115
|
+
? runResult.entries
|
|
116
|
+
: [];
|
|
117
|
+
return entries.map((entry) => ({
|
|
118
|
+
...entry,
|
|
119
|
+
jobId: String(jobId || ""),
|
|
120
|
+
jobName: jobNameById[jobId] || String(jobId || ""),
|
|
121
|
+
}));
|
|
122
|
+
})
|
|
123
|
+
.filter((entry) => Number(entry?.ts || 0) > 0)
|
|
124
|
+
.sort((left, right) => Number(right?.ts || 0) - Number(left?.ts || 0))
|
|
125
|
+
.slice(0, kRecentRunFetchLimit);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const buildCollapsedRunRows = (recentRuns = []) => {
|
|
129
|
+
const rows = [];
|
|
130
|
+
let index = 0;
|
|
131
|
+
while (index < recentRuns.length && rows.length < kRecentRunRowsLimit) {
|
|
132
|
+
const current = recentRuns[index];
|
|
133
|
+
let streakEnd = index + 1;
|
|
134
|
+
while (
|
|
135
|
+
streakEnd < recentRuns.length &&
|
|
136
|
+
String(recentRuns[streakEnd]?.jobId || "") ===
|
|
137
|
+
String(current?.jobId || "")
|
|
138
|
+
) {
|
|
139
|
+
streakEnd += 1;
|
|
140
|
+
}
|
|
141
|
+
const streak = recentRuns.slice(index, streakEnd);
|
|
142
|
+
if (streak.length >= kRecentRunCollapseThreshold) {
|
|
143
|
+
const statusCounts = streak.reduce((accumulator, runEntry) => {
|
|
144
|
+
const status = String(runEntry?.status || "unknown");
|
|
145
|
+
accumulator[status] = Number(accumulator[status] || 0) + 1;
|
|
146
|
+
return accumulator;
|
|
147
|
+
}, {});
|
|
148
|
+
rows.push({
|
|
149
|
+
type: "collapsed-group",
|
|
150
|
+
jobId: String(current?.jobId || ""),
|
|
151
|
+
jobName: String(current?.jobName || current?.jobId || ""),
|
|
152
|
+
count: streak.length,
|
|
153
|
+
newestTs: Number(streak[0]?.ts || 0),
|
|
154
|
+
oldestTs: Number(streak[streak.length - 1]?.ts || 0),
|
|
155
|
+
statusCounts,
|
|
156
|
+
});
|
|
157
|
+
index = streakEnd;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const runEntry of streak) {
|
|
161
|
+
if (rows.length >= kRecentRunRowsLimit) break;
|
|
162
|
+
rows.push({
|
|
163
|
+
type: "entry",
|
|
164
|
+
entry: runEntry,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
index = streakEnd;
|
|
168
|
+
}
|
|
169
|
+
return rows;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const getHashRouteParts = () => {
|
|
173
|
+
const rawHash = String(window.location.hash || "").replace(/^#/, "");
|
|
174
|
+
const hashPath = rawHash || "/cron";
|
|
175
|
+
const [pathPart, queryPart = ""] = hashPath.split("?");
|
|
176
|
+
return {
|
|
177
|
+
pathPart: pathPart || "/cron",
|
|
178
|
+
params: new URLSearchParams(queryPart),
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const readTrendFilterFromHash = () => {
|
|
183
|
+
const { params } = getHashRouteParts();
|
|
184
|
+
const startMs = Number(params.get(kTrendQueryStartKey) || 0);
|
|
185
|
+
const endMs = Number(params.get(kTrendQueryEndKey) || 0);
|
|
186
|
+
const range = String(params.get(kTrendQueryRangeKey) || kTrendRange7d);
|
|
187
|
+
const label = String(params.get(kTrendQueryLabelKey) || "");
|
|
188
|
+
const hasValidRange = range === kTrendRange7d || range === kTrendRange30d;
|
|
189
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
startMs,
|
|
194
|
+
endMs,
|
|
195
|
+
range: hasValidRange ? range : kTrendRange7d,
|
|
196
|
+
label: label || "selected period",
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const writeTrendFilterToHash = (filterValue = null) => {
|
|
201
|
+
const { pathPart, params } = getHashRouteParts();
|
|
202
|
+
if (!filterValue) {
|
|
203
|
+
params.delete(kTrendQueryStartKey);
|
|
204
|
+
params.delete(kTrendQueryEndKey);
|
|
205
|
+
params.delete(kTrendQueryRangeKey);
|
|
206
|
+
params.delete(kTrendQueryLabelKey);
|
|
207
|
+
} else {
|
|
208
|
+
params.set(kTrendQueryStartKey, String(Number(filterValue.startMs || 0)));
|
|
209
|
+
params.set(kTrendQueryEndKey, String(Number(filterValue.endMs || 0)));
|
|
210
|
+
params.set(
|
|
211
|
+
kTrendQueryRangeKey,
|
|
212
|
+
filterValue.range === kTrendRange30d ? kTrendRange30d : kTrendRange7d,
|
|
213
|
+
);
|
|
214
|
+
params.set(kTrendQueryLabelKey, String(filterValue.label || ""));
|
|
215
|
+
}
|
|
216
|
+
const nextQuery = params.toString();
|
|
217
|
+
const nextHash = nextQuery ? `#${pathPart}?${nextQuery}` : `#${pathPart}`;
|
|
218
|
+
const nextUrl =
|
|
219
|
+
`${window.location.pathname}${window.location.search}${nextHash}`;
|
|
220
|
+
window.history.replaceState(window.history.state, "", nextUrl);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const CronOverview = ({
|
|
224
|
+
jobs = [],
|
|
225
|
+
bulkUsageByJobId = {},
|
|
226
|
+
bulkRunsByJobId = {},
|
|
227
|
+
onSelectJob = () => {},
|
|
228
|
+
}) => {
|
|
229
|
+
const [recentRunStatusFilter, setRecentRunStatusFilter] = useState("all");
|
|
230
|
+
const [selectedTrendBucketFilter, setSelectedTrendBucketFilter] = useState(
|
|
231
|
+
() => readTrendFilterFromHash(),
|
|
232
|
+
);
|
|
233
|
+
const enabledCount = jobs.filter((job) => job.enabled !== false).length;
|
|
234
|
+
const disabledCount = jobs.length - enabledCount;
|
|
235
|
+
const nextRunMs = getNextScheduledRunAcrossJobs(jobs);
|
|
236
|
+
const warnings = buildCronOptimizationWarnings(jobs);
|
|
237
|
+
const recentRuns = useMemo(
|
|
238
|
+
() => flattenRecentRuns({ bulkRunsByJobId, jobs }),
|
|
239
|
+
[bulkRunsByJobId, jobs],
|
|
240
|
+
);
|
|
241
|
+
const timeFilteredRecentRuns = useMemo(() => {
|
|
242
|
+
if (!selectedTrendBucketFilter) return recentRuns;
|
|
243
|
+
const startMs = Number(selectedTrendBucketFilter?.startMs || 0);
|
|
244
|
+
const endMs = Number(selectedTrendBucketFilter?.endMs || 0);
|
|
245
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
|
|
246
|
+
return recentRuns;
|
|
247
|
+
}
|
|
248
|
+
return recentRuns.filter((entry) => {
|
|
249
|
+
const timestampMs = Number(entry?.ts || 0);
|
|
250
|
+
return Number.isFinite(timestampMs) && timestampMs >= startMs && timestampMs < endMs;
|
|
251
|
+
});
|
|
252
|
+
}, [recentRuns, selectedTrendBucketFilter]);
|
|
253
|
+
const filteredRecentRuns = useMemo(
|
|
254
|
+
() =>
|
|
255
|
+
timeFilteredRecentRuns.filter((entry) =>
|
|
256
|
+
recentRunStatusFilter === "all"
|
|
257
|
+
? true
|
|
258
|
+
: String(entry?.status || "")
|
|
259
|
+
.trim()
|
|
260
|
+
.toLowerCase() === recentRunStatusFilter,
|
|
261
|
+
),
|
|
262
|
+
[recentRunStatusFilter, timeFilteredRecentRuns],
|
|
263
|
+
);
|
|
264
|
+
const recentRunRows = useMemo(
|
|
265
|
+
() => buildCollapsedRunRows(filteredRecentRuns),
|
|
266
|
+
[filteredRecentRuns],
|
|
267
|
+
);
|
|
268
|
+
const initialTrendRange = selectedTrendBucketFilter?.range === kTrendRange30d
|
|
269
|
+
? kTrendRange30d
|
|
270
|
+
: kTrendRange7d;
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
writeTrendFilterToHash(selectedTrendBucketFilter);
|
|
273
|
+
}, [selectedTrendBucketFilter]);
|
|
274
|
+
|
|
275
|
+
return html`
|
|
276
|
+
<div class="cron-detail-scroll">
|
|
277
|
+
<div class="cron-detail-content">
|
|
278
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
279
|
+
<${SummaryStatCard}
|
|
280
|
+
title="Total jobs"
|
|
281
|
+
value=${jobs.length}
|
|
282
|
+
monospace=${true}
|
|
283
|
+
/>
|
|
284
|
+
<${SummaryStatCard}
|
|
285
|
+
title="Enabled"
|
|
286
|
+
value=${enabledCount}
|
|
287
|
+
monospace=${true}
|
|
288
|
+
/>
|
|
289
|
+
<${SummaryStatCard}
|
|
290
|
+
title="Disabled"
|
|
291
|
+
value=${disabledCount}
|
|
292
|
+
monospace=${true}
|
|
293
|
+
/>
|
|
294
|
+
<${SummaryStatCard}
|
|
295
|
+
title="Next scheduled run"
|
|
296
|
+
value=${nextRunMs ? formatRelativeMs(nextRunMs) : "—"}
|
|
297
|
+
valueClassName="text-sm font-medium text-gray-200 leading-snug"
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<section class="bg-surface border border-border rounded-xl px-4 py-3">
|
|
302
|
+
<details class="group">
|
|
303
|
+
<summary class="list-none cursor-pointer">
|
|
304
|
+
<div class="flex items-center justify-between gap-2">
|
|
305
|
+
<div class="inline-flex items-center gap-2 min-w-0">
|
|
306
|
+
<${ErrorWarningLineIcon}
|
|
307
|
+
className="w-4 h-4 text-yellow-300 shrink-0"
|
|
308
|
+
/>
|
|
309
|
+
<div class="text-xs text-yellow-100 truncate">
|
|
310
|
+
${formatWarningsAttentionText(warnings)}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<span
|
|
314
|
+
class="text-gray-400 text-xs transition-transform group-open:rotate-90"
|
|
315
|
+
>▸</span
|
|
316
|
+
>
|
|
317
|
+
</div>
|
|
318
|
+
</summary>
|
|
319
|
+
<div class="mt-3">
|
|
320
|
+
${warnings.length === 0
|
|
321
|
+
? html`<div class="text-xs text-gray-500">
|
|
322
|
+
No warnings right now.
|
|
323
|
+
</div>`
|
|
324
|
+
: html`
|
|
325
|
+
<div class="space-y-2">
|
|
326
|
+
${warnings.map(
|
|
327
|
+
(warning, index) => html`
|
|
328
|
+
<div
|
|
329
|
+
key=${`warning:${index}`}
|
|
330
|
+
class=${`rounded-xl border p-3 text-xs ${warningClassName(warning.tone)} ${warning?.jobId ? "cursor-pointer hover:brightness-110" : ""}`}
|
|
331
|
+
role=${warning?.jobId ? "button" : null}
|
|
332
|
+
tabindex=${warning?.jobId ? "0" : null}
|
|
333
|
+
onclick=${() => {
|
|
334
|
+
if (!warning?.jobId) return;
|
|
335
|
+
onSelectJob(warning.jobId);
|
|
336
|
+
}}
|
|
337
|
+
onKeyDown=${(event) => {
|
|
338
|
+
if (!warning?.jobId) return;
|
|
339
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
340
|
+
return;
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
onSelectJob(warning.jobId);
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<div class="font-medium">${warning.title}</div>
|
|
346
|
+
<div class="mt-1 opacity-90">${warning.body}</div>
|
|
347
|
+
</div>
|
|
348
|
+
`,
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
`}
|
|
352
|
+
</div>
|
|
353
|
+
</details>
|
|
354
|
+
</section>
|
|
355
|
+
|
|
356
|
+
<${CronCalendar}
|
|
357
|
+
jobs=${jobs}
|
|
358
|
+
usageByJobId=${bulkUsageByJobId}
|
|
359
|
+
runsByJobId=${bulkRunsByJobId}
|
|
360
|
+
onSelectJob=${onSelectJob}
|
|
361
|
+
/>
|
|
362
|
+
|
|
363
|
+
<${CronRunsTrendCard}
|
|
364
|
+
bulkRunsByJobId=${bulkRunsByJobId}
|
|
365
|
+
initialRange=${initialTrendRange}
|
|
366
|
+
selectedBucketFilter=${selectedTrendBucketFilter}
|
|
367
|
+
onBucketFilterChange=${setSelectedTrendBucketFilter}
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
<section
|
|
371
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
372
|
+
>
|
|
373
|
+
<div class="flex items-start justify-between gap-3">
|
|
374
|
+
<div class="inline-flex items-center gap-3">
|
|
375
|
+
<h3 class="card-label card-label-bright">Run history</h3>
|
|
376
|
+
<div class="text-xs text-gray-500">
|
|
377
|
+
${formatTokenCount(filteredRecentRuns.length)} entries
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="shrink-0">
|
|
381
|
+
<${SegmentedControl}
|
|
382
|
+
options=${kRunStatusFilterOptions}
|
|
383
|
+
value=${recentRunStatusFilter}
|
|
384
|
+
onChange=${setRecentRunStatusFilter}
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
${selectedTrendBucketFilter
|
|
389
|
+
? html`
|
|
390
|
+
<div class="flex items-center">
|
|
391
|
+
<span class="inline-flex items-center gap-1.5 text-xs pl-2.5 pr-2 py-1 rounded-full border border-border text-gray-300 bg-black/20">
|
|
392
|
+
Filtered to ${selectedTrendBucketFilter.label}
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
class="text-gray-500 hover:text-gray-200 leading-none"
|
|
396
|
+
onclick=${() => setSelectedTrendBucketFilter(null)}
|
|
397
|
+
aria-label="Clear trend filter"
|
|
398
|
+
>
|
|
399
|
+
×
|
|
400
|
+
</button>
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
`
|
|
404
|
+
: null}
|
|
405
|
+
${recentRunRows.length === 0
|
|
406
|
+
? html`<div class="text-sm text-gray-500">No runs found.</div>`
|
|
407
|
+
: html`
|
|
408
|
+
<div class="ac-history-list">
|
|
409
|
+
${recentRunRows.map((row, rowIndex) => {
|
|
410
|
+
if (row.type === "collapsed-group") {
|
|
411
|
+
const statusSummary = Object.entries(
|
|
412
|
+
row.statusCounts || {},
|
|
413
|
+
)
|
|
414
|
+
.map(([status, count]) => `${status}: ${count}`)
|
|
415
|
+
.join(" • ");
|
|
416
|
+
const timeRangeLabel = `[${formatRecentRunTimestamp(row.oldestTs)} - ${formatRecentRunTimestamp(row.newestTs)}]`;
|
|
417
|
+
return html`
|
|
418
|
+
<details
|
|
419
|
+
key=${`collapsed:${rowIndex}:${row.jobId}`}
|
|
420
|
+
class="ac-history-item"
|
|
421
|
+
>
|
|
422
|
+
<summary class="ac-history-summary">
|
|
423
|
+
<div class="ac-history-summary-row">
|
|
424
|
+
<span
|
|
425
|
+
class="inline-flex items-center gap-2 min-w-0"
|
|
426
|
+
>
|
|
427
|
+
<span
|
|
428
|
+
class="ac-history-toggle shrink-0"
|
|
429
|
+
aria-hidden="true"
|
|
430
|
+
>▸</span
|
|
431
|
+
>
|
|
432
|
+
<span class="truncate text-xs text-gray-300">
|
|
433
|
+
${row.jobName} -
|
|
434
|
+
${formatTokenCount(row.count)} runs -
|
|
435
|
+
${timeRangeLabel}
|
|
436
|
+
</span>
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
</summary>
|
|
440
|
+
<div class="ac-history-body space-y-2 text-xs">
|
|
441
|
+
<div class="text-gray-500">
|
|
442
|
+
${formatTokenCount(row.count)} consecutive runs
|
|
443
|
+
collapsed (${timeRangeLabel})
|
|
444
|
+
</div>
|
|
445
|
+
<div class="text-gray-500">
|
|
446
|
+
Statuses: ${statusSummary}
|
|
447
|
+
</div>
|
|
448
|
+
<div>
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
452
|
+
onclick=${() => onSelectJob(row.jobId)}
|
|
453
|
+
>
|
|
454
|
+
Open ${row.jobName}
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</details>
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
const runEntry = row.entry || {};
|
|
462
|
+
const runStatus = String(runEntry?.status || "unknown");
|
|
463
|
+
const runUsage = runEntry?.usage || {};
|
|
464
|
+
const runInputTokens = Number(
|
|
465
|
+
runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0,
|
|
466
|
+
);
|
|
467
|
+
const runOutputTokens = Number(
|
|
468
|
+
runUsage?.output_tokens ?? runUsage?.outputTokens ?? 0,
|
|
469
|
+
);
|
|
470
|
+
const runTokens = Number(
|
|
471
|
+
runUsage?.total_tokens ?? runUsage?.totalTokens ?? 0,
|
|
472
|
+
);
|
|
473
|
+
const runEstimatedCost = getRunEstimatedCost(runEntry);
|
|
474
|
+
const runTitle = String(runEntry?.jobName || "").trim();
|
|
475
|
+
const hasRunTitle = runTitle.length > 0;
|
|
476
|
+
return html`
|
|
477
|
+
<details
|
|
478
|
+
key=${`entry:${rowIndex}:${runEntry.ts}:${runEntry.jobId || ""}`}
|
|
479
|
+
class="ac-history-item"
|
|
480
|
+
>
|
|
481
|
+
<summary class="ac-history-summary">
|
|
482
|
+
<div class="ac-history-summary-row">
|
|
483
|
+
<span
|
|
484
|
+
class="inline-flex items-center gap-2 min-w-0"
|
|
485
|
+
>
|
|
486
|
+
<span
|
|
487
|
+
class="ac-history-toggle shrink-0"
|
|
488
|
+
aria-hidden="true"
|
|
489
|
+
>▸</span
|
|
490
|
+
>
|
|
491
|
+
${hasRunTitle
|
|
492
|
+
? html`
|
|
493
|
+
<span
|
|
494
|
+
class="inline-flex items-center gap-2 min-w-0"
|
|
495
|
+
>
|
|
496
|
+
<span class="truncate text-xs text-gray-300">
|
|
497
|
+
${runTitle}
|
|
498
|
+
</span>
|
|
499
|
+
<span class="text-xs text-gray-500 shrink-0">
|
|
500
|
+
${formatRecentRunTimestamp(runEntry.ts)}
|
|
501
|
+
</span>
|
|
502
|
+
</span>
|
|
503
|
+
`
|
|
504
|
+
: html`
|
|
505
|
+
<span class="truncate text-xs text-gray-300">
|
|
506
|
+
${runEntry.jobId} -
|
|
507
|
+
${formatRecentRunTimestamp(runEntry.ts)}
|
|
508
|
+
</span>
|
|
509
|
+
`}
|
|
510
|
+
</span>
|
|
511
|
+
<span
|
|
512
|
+
class="inline-flex items-center gap-3 shrink-0 text-xs"
|
|
513
|
+
>
|
|
514
|
+
<span class=${runStatusClassName(runStatus)}
|
|
515
|
+
>${runStatus}</span
|
|
516
|
+
>
|
|
517
|
+
<span class="text-gray-400"
|
|
518
|
+
>${formatDurationCompactMs(
|
|
519
|
+
runEntry.durationMs,
|
|
520
|
+
)}</span
|
|
521
|
+
>
|
|
522
|
+
<span class="text-gray-400"
|
|
523
|
+
>${formatTokenCount(runTokens)} tk</span
|
|
524
|
+
>
|
|
525
|
+
<span class="text-gray-500"
|
|
526
|
+
>${runEstimatedCost == null
|
|
527
|
+
? "—"
|
|
528
|
+
: `~${formatCost(runEstimatedCost)}`}</span
|
|
529
|
+
>
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
</summary>
|
|
533
|
+
<div class="ac-history-body space-y-2 text-xs">
|
|
534
|
+
${runEntry.summary
|
|
535
|
+
? html`<div>
|
|
536
|
+
<span class="text-gray-500">Summary:</span>
|
|
537
|
+
${runEntry.summary}
|
|
538
|
+
</div>`
|
|
539
|
+
: null}
|
|
540
|
+
${runEntry.error
|
|
541
|
+
? html`<div class="text-red-300">
|
|
542
|
+
<span class="text-gray-500">Error:</span>
|
|
543
|
+
${runEntry.error}
|
|
544
|
+
</div>`
|
|
545
|
+
: null}
|
|
546
|
+
<div class="ac-surface-inset rounded-lg p-2.5 space-y-1.5">
|
|
547
|
+
<div class="text-gray-500">
|
|
548
|
+
Model:
|
|
549
|
+
<span class="text-gray-300 font-mono"
|
|
550
|
+
>${runEntry.model || "—"}</span
|
|
551
|
+
>
|
|
552
|
+
${runEntry.sessionKey
|
|
553
|
+
? html` | Session:
|
|
554
|
+
<span class="text-gray-300 font-mono"
|
|
555
|
+
>${runEntry.sessionKey}</span
|
|
556
|
+
>`
|
|
557
|
+
: null}
|
|
558
|
+
</div>
|
|
559
|
+
<div class="text-gray-500">
|
|
560
|
+
Usage:
|
|
561
|
+
<span class="text-gray-300"
|
|
562
|
+
>${formatTokenCount(runInputTokens)} in</span
|
|
563
|
+
>
|
|
564
|
+
<span class="text-gray-600">•</span>
|
|
565
|
+
<span class="text-gray-300"
|
|
566
|
+
>${formatTokenCount(runOutputTokens)} out</span
|
|
567
|
+
>
|
|
568
|
+
<span class="text-gray-600">•</span>
|
|
569
|
+
<span class="text-gray-300"
|
|
570
|
+
>${formatTokenCount(runTokens)} tk</span
|
|
571
|
+
>
|
|
572
|
+
<span class="text-gray-600">•</span>
|
|
573
|
+
<span class="text-gray-300"
|
|
574
|
+
>${runEstimatedCost == null
|
|
575
|
+
? "—"
|
|
576
|
+
: `~${formatCost(runEstimatedCost)}`}</span
|
|
577
|
+
>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
<div>
|
|
581
|
+
<button
|
|
582
|
+
type="button"
|
|
583
|
+
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
584
|
+
onclick=${() => onSelectJob(runEntry.jobId)}
|
|
585
|
+
>
|
|
586
|
+
Open ${runEntry.jobName || runEntry.jobId}
|
|
587
|
+
</button>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
</details>
|
|
591
|
+
`;
|
|
592
|
+
})}
|
|
593
|
+
</div>
|
|
594
|
+
`}
|
|
595
|
+
</section>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
`;
|
|
599
|
+
};
|