@chrysb/alphaclaw 0.6.2-beta.5 → 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/css/cron.css +91 -39
- 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/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/welcome-pairing-step.js +88 -59
- package/package.json +1 -1
|
@@ -3,13 +3,13 @@ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
buildCronOptimizationWarnings,
|
|
6
|
-
formatRelativeMs,
|
|
7
6
|
formatTokenCount,
|
|
8
7
|
getNextScheduledRunAcrossJobs,
|
|
9
8
|
} from "./cron-helpers.js";
|
|
10
9
|
import { CronCalendar } from "./cron-calendar.js";
|
|
11
10
|
import { CronRunsTrendCard } from "./cron-runs-trend-card.js";
|
|
12
11
|
import { CronRunHistoryPanel } from "./cron-run-history-panel.js";
|
|
12
|
+
import { CronInsightsPanel } from "./cron-insights-panel.js";
|
|
13
13
|
import { SummaryStatCard } from "../summary-stat-card.js";
|
|
14
14
|
import { ErrorWarningLineIcon } from "../icons.js";
|
|
15
15
|
|
|
@@ -17,6 +17,7 @@ const html = htm.bind(h);
|
|
|
17
17
|
const kRecentRunFetchLimit = 100;
|
|
18
18
|
const kRecentRunRowsLimit = 20;
|
|
19
19
|
const kRecentRunCollapseThreshold = 5;
|
|
20
|
+
const kTrendRange24h = "24h";
|
|
20
21
|
const kTrendRange7d = "7d";
|
|
21
22
|
const kTrendRange30d = "30d";
|
|
22
23
|
const kTrendQueryStartKey = "trendStart";
|
|
@@ -136,16 +137,21 @@ const readTrendFilterFromHash = () => {
|
|
|
136
137
|
const { params } = getHashRouteParts();
|
|
137
138
|
const startMs = Number(params.get(kTrendQueryStartKey) || 0);
|
|
138
139
|
const endMs = Number(params.get(kTrendQueryEndKey) || 0);
|
|
139
|
-
const range = String(params.get(kTrendQueryRangeKey) ||
|
|
140
|
+
const range = String(params.get(kTrendQueryRangeKey) || kTrendRange24h);
|
|
140
141
|
const label = String(params.get(kTrendQueryLabelKey) || "");
|
|
141
|
-
const hasValidRange =
|
|
142
|
-
|
|
142
|
+
const hasValidRange =
|
|
143
|
+
range === kTrendRange24h || range === kTrendRange7d || range === kTrendRange30d;
|
|
144
|
+
if (
|
|
145
|
+
!Number.isFinite(startMs) ||
|
|
146
|
+
!Number.isFinite(endMs) ||
|
|
147
|
+
endMs <= startMs
|
|
148
|
+
) {
|
|
143
149
|
return null;
|
|
144
150
|
}
|
|
145
151
|
return {
|
|
146
152
|
startMs,
|
|
147
153
|
endMs,
|
|
148
|
-
range: hasValidRange ? range :
|
|
154
|
+
range: hasValidRange ? range : kTrendRange24h,
|
|
149
155
|
label: label || "selected period",
|
|
150
156
|
};
|
|
151
157
|
};
|
|
@@ -162,14 +168,17 @@ const writeTrendFilterToHash = (filterValue = null) => {
|
|
|
162
168
|
params.set(kTrendQueryEndKey, String(Number(filterValue.endMs || 0)));
|
|
163
169
|
params.set(
|
|
164
170
|
kTrendQueryRangeKey,
|
|
165
|
-
filterValue.range === kTrendRange30d
|
|
171
|
+
filterValue.range === kTrendRange30d
|
|
172
|
+
? kTrendRange30d
|
|
173
|
+
: filterValue.range === kTrendRange7d
|
|
174
|
+
? kTrendRange7d
|
|
175
|
+
: kTrendRange24h,
|
|
166
176
|
);
|
|
167
177
|
params.set(kTrendQueryLabelKey, String(filterValue.label || ""));
|
|
168
178
|
}
|
|
169
179
|
const nextQuery = params.toString();
|
|
170
180
|
const nextHash = nextQuery ? `#${pathPart}?${nextQuery}` : `#${pathPart}`;
|
|
171
|
-
const nextUrl =
|
|
172
|
-
`${window.location.pathname}${window.location.search}${nextHash}`;
|
|
181
|
+
const nextUrl = `${window.location.pathname}${window.location.search}${nextHash}`;
|
|
173
182
|
window.history.replaceState(window.history.state, "", nextUrl);
|
|
174
183
|
};
|
|
175
184
|
|
|
@@ -195,12 +204,20 @@ export const CronOverview = ({
|
|
|
195
204
|
if (!selectedTrendBucketFilter) return recentRuns;
|
|
196
205
|
const startMs = Number(selectedTrendBucketFilter?.startMs || 0);
|
|
197
206
|
const endMs = Number(selectedTrendBucketFilter?.endMs || 0);
|
|
198
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
!Number.isFinite(startMs) ||
|
|
209
|
+
!Number.isFinite(endMs) ||
|
|
210
|
+
endMs <= startMs
|
|
211
|
+
) {
|
|
199
212
|
return recentRuns;
|
|
200
213
|
}
|
|
201
214
|
return recentRuns.filter((entry) => {
|
|
202
215
|
const timestampMs = Number(entry?.ts || 0);
|
|
203
|
-
return
|
|
216
|
+
return (
|
|
217
|
+
Number.isFinite(timestampMs) &&
|
|
218
|
+
timestampMs >= startMs &&
|
|
219
|
+
timestampMs < endMs
|
|
220
|
+
);
|
|
204
221
|
});
|
|
205
222
|
}, [recentRuns, selectedTrendBucketFilter]);
|
|
206
223
|
const filteredRecentRuns = useMemo(
|
|
@@ -218,9 +235,12 @@ export const CronOverview = ({
|
|
|
218
235
|
() => buildCollapsedRunRows(filteredRecentRuns),
|
|
219
236
|
[filteredRecentRuns],
|
|
220
237
|
);
|
|
221
|
-
const initialTrendRange =
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
const initialTrendRange =
|
|
239
|
+
selectedTrendBucketFilter?.range === kTrendRange30d
|
|
240
|
+
? kTrendRange30d
|
|
241
|
+
: selectedTrendBucketFilter?.range === kTrendRange7d
|
|
242
|
+
? kTrendRange7d
|
|
243
|
+
: kTrendRange24h;
|
|
224
244
|
useEffect(() => {
|
|
225
245
|
writeTrendFilterToHash(selectedTrendBucketFilter);
|
|
226
246
|
}, [selectedTrendBucketFilter]);
|
|
@@ -228,7 +248,7 @@ export const CronOverview = ({
|
|
|
228
248
|
return html`
|
|
229
249
|
<div class="cron-detail-scroll">
|
|
230
250
|
<div class="cron-detail-content">
|
|
231
|
-
<div class="grid grid-cols-1 md:grid-cols-
|
|
251
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
232
252
|
<${SummaryStatCard}
|
|
233
253
|
title="Total jobs"
|
|
234
254
|
value=${jobs.length}
|
|
@@ -244,11 +264,6 @@ export const CronOverview = ({
|
|
|
244
264
|
value=${disabledCount}
|
|
245
265
|
monospace=${true}
|
|
246
266
|
/>
|
|
247
|
-
<${SummaryStatCard}
|
|
248
|
-
title="Next scheduled run"
|
|
249
|
-
value=${nextRunMs ? formatRelativeMs(nextRunMs) : "—"}
|
|
250
|
-
valueClassName="text-sm font-medium text-gray-200 leading-snug"
|
|
251
|
-
/>
|
|
252
267
|
</div>
|
|
253
268
|
|
|
254
269
|
<section class="bg-surface border border-border rounded-xl px-4 py-3">
|
|
@@ -320,6 +335,12 @@ export const CronOverview = ({
|
|
|
320
335
|
onBucketFilterChange=${setSelectedTrendBucketFilter}
|
|
321
336
|
/>
|
|
322
337
|
|
|
338
|
+
<${CronInsightsPanel}
|
|
339
|
+
jobs=${jobs}
|
|
340
|
+
bulkRunsByJobId=${bulkRunsByJobId}
|
|
341
|
+
onSelectJob=${onSelectJob}
|
|
342
|
+
/>
|
|
343
|
+
|
|
323
344
|
<${CronRunHistoryPanel}
|
|
324
345
|
entryCountLabel=${`${formatTokenCount(filteredRecentRuns.length)} entries`}
|
|
325
346
|
primaryFilterOptions=${kRunStatusFilterOptions}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { EditorSurface } from "../file-viewer/editor-surface.js";
|
|
5
|
+
import { countTextLines, shouldUseSimpleEditorMode } from "../file-viewer/utils.js";
|
|
6
|
+
import {
|
|
7
|
+
kLargeFileSimpleEditorCharThreshold,
|
|
8
|
+
kLargeFileSimpleEditorLineThreshold,
|
|
9
|
+
} from "../file-viewer/constants.js";
|
|
10
|
+
import { useEditorLineNumberSync } from "../file-viewer/use-editor-line-number-sync.js";
|
|
11
|
+
import { highlightEditorLines } from "../../lib/syntax-highlighters/index.js";
|
|
12
|
+
import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
|
|
13
|
+
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
const kCronPromptEditorHeightUiSettingKey = "cronPromptEditorHeightPx";
|
|
16
|
+
const kCronPromptEditorDefaultHeightPx = 280;
|
|
17
|
+
const kCronPromptEditorMinHeightPx = 180;
|
|
18
|
+
|
|
19
|
+
const clampPromptEditorHeight = (value) => {
|
|
20
|
+
const parsed = Number(value);
|
|
21
|
+
const normalized = Number.isFinite(parsed)
|
|
22
|
+
? Math.round(parsed)
|
|
23
|
+
: kCronPromptEditorDefaultHeightPx;
|
|
24
|
+
return Math.max(kCronPromptEditorMinHeightPx, normalized);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readCssHeightPx = (element) => {
|
|
28
|
+
if (!element) return 0;
|
|
29
|
+
const computedHeight = Number.parseFloat(
|
|
30
|
+
window.getComputedStyle(element).height || "0",
|
|
31
|
+
);
|
|
32
|
+
return Number.isFinite(computedHeight) ? computedHeight : 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const CronPromptEditor = ({
|
|
36
|
+
promptValue = "",
|
|
37
|
+
savedPromptValue = "",
|
|
38
|
+
onChangePrompt = () => {},
|
|
39
|
+
onSaveChanges = () => {},
|
|
40
|
+
}) => {
|
|
41
|
+
const promptEditorShellRef = useRef(null);
|
|
42
|
+
const editorTextareaRef = useRef(null);
|
|
43
|
+
const editorLineNumbersRef = useRef(null);
|
|
44
|
+
const editorLineNumberRowRefs = useRef([]);
|
|
45
|
+
const editorHighlightRef = useRef(null);
|
|
46
|
+
const editorHighlightLineRefs = useRef([]);
|
|
47
|
+
const [promptEditorHeightPx, setPromptEditorHeightPx] = useState(() => {
|
|
48
|
+
const settings = readUiSettings();
|
|
49
|
+
return clampPromptEditorHeight(
|
|
50
|
+
settings?.[kCronPromptEditorHeightUiSettingKey],
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const lineCount = countTextLines(promptValue);
|
|
55
|
+
const shouldUseHighlightedEditor = !shouldUseSimpleEditorMode({
|
|
56
|
+
contentLength: promptValue.length,
|
|
57
|
+
lineCount,
|
|
58
|
+
charThreshold: kLargeFileSimpleEditorCharThreshold,
|
|
59
|
+
lineThreshold: kLargeFileSimpleEditorLineThreshold,
|
|
60
|
+
});
|
|
61
|
+
const highlightedEditorLines = useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
shouldUseHighlightedEditor
|
|
64
|
+
? highlightEditorLines(promptValue, "markdown")
|
|
65
|
+
: [],
|
|
66
|
+
[promptValue, shouldUseHighlightedEditor],
|
|
67
|
+
);
|
|
68
|
+
const editorLineCount = Math.max(
|
|
69
|
+
lineCount,
|
|
70
|
+
Array.isArray(highlightedEditorLines) ? highlightedEditorLines.length : 0,
|
|
71
|
+
);
|
|
72
|
+
const editorLineNumbers = useMemo(
|
|
73
|
+
() => Array.from({ length: editorLineCount }, (_, index) => index + 1),
|
|
74
|
+
[editorLineCount],
|
|
75
|
+
);
|
|
76
|
+
const isDirty = promptValue !== savedPromptValue;
|
|
77
|
+
|
|
78
|
+
useEditorLineNumberSync({
|
|
79
|
+
enabled: shouldUseHighlightedEditor,
|
|
80
|
+
syncKey: `${promptValue.length}:${highlightedEditorLines.length}`,
|
|
81
|
+
editorLineNumberRowRefs,
|
|
82
|
+
editorHighlightLineRefs,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const handleEditorScroll = (event) => {
|
|
86
|
+
const scrollTop = event.currentTarget.scrollTop;
|
|
87
|
+
if (editorLineNumbersRef.current)
|
|
88
|
+
editorLineNumbersRef.current.scrollTop = scrollTop;
|
|
89
|
+
if (editorHighlightRef.current) {
|
|
90
|
+
editorHighlightRef.current.scrollTop = scrollTop;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleEditorKeyDown = (event) => {
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
onSaveChanges();
|
|
98
|
+
}
|
|
99
|
+
if (event.key === "Tab") {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
const textarea = editorTextareaRef.current;
|
|
102
|
+
if (!textarea) return;
|
|
103
|
+
const start = textarea.selectionStart;
|
|
104
|
+
const end = textarea.selectionEnd;
|
|
105
|
+
const nextValue = `${promptValue.slice(0, start)} ${promptValue.slice(end)}`;
|
|
106
|
+
onChangePrompt(nextValue);
|
|
107
|
+
window.requestAnimationFrame(() => {
|
|
108
|
+
textarea.selectionStart = start + 2;
|
|
109
|
+
textarea.selectionEnd = start + 2;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const shellElement = promptEditorShellRef.current;
|
|
116
|
+
if (!shellElement || typeof ResizeObserver === "undefined") return () => {};
|
|
117
|
+
|
|
118
|
+
let saveTimer = null;
|
|
119
|
+
const observer = new ResizeObserver((entries) => {
|
|
120
|
+
const entry = entries?.[0];
|
|
121
|
+
const nextHeight = clampPromptEditorHeight(readCssHeightPx(entry?.target));
|
|
122
|
+
setPromptEditorHeightPx((currentValue) =>
|
|
123
|
+
Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue,
|
|
124
|
+
);
|
|
125
|
+
if (saveTimer) window.clearTimeout(saveTimer);
|
|
126
|
+
saveTimer = window.setTimeout(() => {
|
|
127
|
+
const settings = readUiSettings();
|
|
128
|
+
settings[kCronPromptEditorHeightUiSettingKey] = nextHeight;
|
|
129
|
+
writeUiSettings(settings);
|
|
130
|
+
}, 120);
|
|
131
|
+
});
|
|
132
|
+
observer.observe(shellElement);
|
|
133
|
+
return () => {
|
|
134
|
+
observer.disconnect();
|
|
135
|
+
if (saveTimer) window.clearTimeout(saveTimer);
|
|
136
|
+
};
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
return html`
|
|
140
|
+
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
141
|
+
<div class="flex items-center justify-between gap-2">
|
|
142
|
+
<h3 class="card-label inline-flex items-center gap-1.5">
|
|
143
|
+
Prompt
|
|
144
|
+
${isDirty ? html`<span class="file-viewer-dirty-dot"></span>` : null}
|
|
145
|
+
</h3>
|
|
146
|
+
</div>
|
|
147
|
+
<div
|
|
148
|
+
class="cron-prompt-editor-shell"
|
|
149
|
+
ref=${promptEditorShellRef}
|
|
150
|
+
style=${{ height: `${promptEditorHeightPx}px` }}
|
|
151
|
+
>
|
|
152
|
+
<${EditorSurface}
|
|
153
|
+
editorShellClassName="file-viewer-editor-shell"
|
|
154
|
+
editorLineNumbers=${editorLineNumbers}
|
|
155
|
+
editorLineNumbersRef=${editorLineNumbersRef}
|
|
156
|
+
editorLineNumberRowRefs=${editorLineNumberRowRefs}
|
|
157
|
+
shouldUseHighlightedEditor=${shouldUseHighlightedEditor}
|
|
158
|
+
highlightedEditorLines=${highlightedEditorLines}
|
|
159
|
+
editorHighlightRef=${editorHighlightRef}
|
|
160
|
+
editorHighlightLineRefs=${editorHighlightLineRefs}
|
|
161
|
+
editorTextareaRef=${editorTextareaRef}
|
|
162
|
+
renderContent=${promptValue}
|
|
163
|
+
handleContentInput=${(event) => onChangePrompt(event.target.value)}
|
|
164
|
+
handleEditorKeyDown=${handleEditorKeyDown}
|
|
165
|
+
handleEditorScroll=${handleEditorScroll}
|
|
166
|
+
handleEditorSelectionChange=${() => {}}
|
|
167
|
+
isEditBlocked=${false}
|
|
168
|
+
isPreviewOnly=${false}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
`;
|
|
173
|
+
};
|
|
@@ -17,7 +17,8 @@ const runStatusClassName = (status = "") => {
|
|
|
17
17
|
if (normalized === "skipped") return "text-yellow-300";
|
|
18
18
|
return "text-gray-400";
|
|
19
19
|
};
|
|
20
|
-
const runDeliveryLabel = (run) =>
|
|
20
|
+
const runDeliveryLabel = (run) =>
|
|
21
|
+
String(run?.deliveryStatus || "not-requested");
|
|
21
22
|
const getRunEstimatedCost = (runEntry = {}) => {
|
|
22
23
|
const parsed = Number(runEntry?.estimatedCost);
|
|
23
24
|
return Number.isFinite(parsed) ? parsed : null;
|
|
@@ -26,7 +27,10 @@ const formatOverviewTimestamp = (timestampMs) =>
|
|
|
26
27
|
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
27
28
|
fallback: "—",
|
|
28
29
|
valueIsEpochMs: true,
|
|
29
|
-
}).replace(
|
|
30
|
+
}).replace(
|
|
31
|
+
/\s([AP])M\b/g,
|
|
32
|
+
(_, marker) => `${String(marker || "").toLowerCase()}m`,
|
|
33
|
+
);
|
|
30
34
|
const formatDetailTimestamp = (timestampMs) =>
|
|
31
35
|
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
32
36
|
fallback: "—",
|
|
@@ -36,11 +40,7 @@ const formatRowTimestamp = (timestampMs, variant = "overview") =>
|
|
|
36
40
|
variant === "detail"
|
|
37
41
|
? formatDetailTimestamp(timestampMs)
|
|
38
42
|
: formatOverviewTimestamp(timestampMs);
|
|
39
|
-
const renderCollapsedGroupRow = ({
|
|
40
|
-
row,
|
|
41
|
-
rowIndex,
|
|
42
|
-
onSelectJob = () => {},
|
|
43
|
-
}) => {
|
|
43
|
+
const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
44
44
|
const statusSummary = Object.entries(row.statusCounts || {})
|
|
45
45
|
.map(([status, count]) => `${status}: ${count}`)
|
|
46
46
|
.join(" • ");
|
|
@@ -52,17 +52,10 @@ const renderCollapsedGroupRow = ({
|
|
|
52
52
|
>
|
|
53
53
|
<summary class="ac-history-summary">
|
|
54
54
|
<div class="ac-history-summary-row">
|
|
55
|
-
<span
|
|
56
|
-
class="
|
|
57
|
-
>
|
|
58
|
-
<span
|
|
59
|
-
class="ac-history-toggle shrink-0"
|
|
60
|
-
aria-hidden="true"
|
|
61
|
-
>▸</span
|
|
62
|
-
>
|
|
55
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
56
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
63
57
|
<span class="truncate text-xs text-gray-300">
|
|
64
|
-
${row.jobName} -
|
|
65
|
-
${formatTokenCount(row.count)} runs -
|
|
58
|
+
${row.jobName} - ${formatTokenCount(row.count)} runs -
|
|
66
59
|
${timeRangeLabel}
|
|
67
60
|
</span>
|
|
68
61
|
</span>
|
|
@@ -70,12 +63,10 @@ const renderCollapsedGroupRow = ({
|
|
|
70
63
|
</summary>
|
|
71
64
|
<div class="ac-history-body space-y-2 text-xs">
|
|
72
65
|
<div class="text-gray-500">
|
|
73
|
-
${formatTokenCount(row.count)} consecutive runs
|
|
74
|
-
|
|
75
|
-
</div>
|
|
76
|
-
<div class="text-gray-500">
|
|
77
|
-
Statuses: ${statusSummary}
|
|
66
|
+
${formatTokenCount(row.count)} consecutive runs collapsed
|
|
67
|
+
(${timeRangeLabel})
|
|
78
68
|
</div>
|
|
69
|
+
<div class="text-gray-500">Statuses: ${statusSummary}</div>
|
|
79
70
|
${row?.jobId
|
|
80
71
|
? html`
|
|
81
72
|
<div>
|
|
@@ -103,9 +94,15 @@ const renderEntryRow = ({
|
|
|
103
94
|
const runEntry = row?.entry || row || {};
|
|
104
95
|
const runUsage = runEntry?.usage || {};
|
|
105
96
|
const runStatus = String(runEntry?.status || "unknown");
|
|
106
|
-
const runInputTokens = Number(
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
const runInputTokens = Number(
|
|
98
|
+
runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0,
|
|
99
|
+
);
|
|
100
|
+
const runOutputTokens = Number(
|
|
101
|
+
runUsage?.output_tokens ?? runUsage?.outputTokens ?? 0,
|
|
102
|
+
);
|
|
103
|
+
const runTokens = Number(
|
|
104
|
+
runUsage?.total_tokens ?? runUsage?.totalTokens ?? 0,
|
|
105
|
+
);
|
|
109
106
|
const runEstimatedCost = getRunEstimatedCost(runEntry);
|
|
110
107
|
const runTitle = String(runEntry?.jobName || "").trim();
|
|
111
108
|
const hasRunTitle = runTitle.length > 0;
|
|
@@ -117,14 +114,8 @@ const renderEntryRow = ({
|
|
|
117
114
|
>
|
|
118
115
|
<summary class="ac-history-summary">
|
|
119
116
|
<div class="ac-history-summary-row">
|
|
120
|
-
<span
|
|
121
|
-
class="
|
|
122
|
-
>
|
|
123
|
-
<span
|
|
124
|
-
class="ac-history-toggle shrink-0"
|
|
125
|
-
aria-hidden="true"
|
|
126
|
-
>▸</span
|
|
127
|
-
>
|
|
117
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
118
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
128
119
|
${isDetail
|
|
129
120
|
? html`
|
|
130
121
|
<span class="truncate text-xs text-gray-300">
|
|
@@ -134,7 +125,9 @@ const renderEntryRow = ({
|
|
|
134
125
|
: hasRunTitle
|
|
135
126
|
? html`
|
|
136
127
|
<span class="inline-flex items-center gap-2 min-w-0">
|
|
137
|
-
<span class="truncate text-xs text-gray-300"
|
|
128
|
+
<span class="truncate text-xs text-gray-300"
|
|
129
|
+
>${runTitle}</span
|
|
130
|
+
>
|
|
138
131
|
<span class="text-xs text-gray-500 shrink-0">
|
|
139
132
|
${formatRowTimestamp(runEntry.ts, variant)}
|
|
140
133
|
</span>
|
|
@@ -147,17 +140,21 @@ const renderEntryRow = ({
|
|
|
147
140
|
</span>
|
|
148
141
|
`}
|
|
149
142
|
</span>
|
|
150
|
-
<span
|
|
151
|
-
class="inline-flex items-center gap-3 shrink-0 text-xs"
|
|
152
|
-
>
|
|
143
|
+
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
153
144
|
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
154
|
-
<span class="text-gray-400"
|
|
145
|
+
<span class="text-gray-400"
|
|
146
|
+
>${formatDurationCompactMs(runEntry.durationMs)}</span
|
|
147
|
+
>
|
|
155
148
|
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
156
149
|
${isDetail
|
|
157
|
-
? html`<span class="text-gray-500"
|
|
150
|
+
? html`<span class="text-gray-500"
|
|
151
|
+
>${runDeliveryLabel(runEntry)}</span
|
|
152
|
+
>`
|
|
158
153
|
: html`
|
|
159
154
|
<span class="text-gray-500"
|
|
160
|
-
>${runEstimatedCost == null
|
|
155
|
+
>${runEstimatedCost == null
|
|
156
|
+
? "—"
|
|
157
|
+
: `~${formatCost(runEstimatedCost)}`}</span
|
|
161
158
|
>
|
|
162
159
|
`}
|
|
163
160
|
</span>
|
|
@@ -165,36 +162,50 @@ const renderEntryRow = ({
|
|
|
165
162
|
</summary>
|
|
166
163
|
<div class="ac-history-body space-y-2 text-xs">
|
|
167
164
|
${runEntry.summary
|
|
168
|
-
? html`<div
|
|
165
|
+
? html`<div>
|
|
166
|
+
<span class="text-gray-500">Summary:</span> ${runEntry.summary}
|
|
167
|
+
</div>`
|
|
169
168
|
: null}
|
|
170
169
|
${runEntry.error
|
|
171
|
-
? html`<div class="text-red-300"
|
|
170
|
+
? html`<div class="text-red-300">
|
|
171
|
+
<span class="text-gray-500">Error:</span> ${runEntry.error}
|
|
172
|
+
</div>`
|
|
172
173
|
: null}
|
|
173
174
|
<div class="ac-surface-inset rounded-lg p-2.5 space-y-1.5">
|
|
174
175
|
<div class="text-gray-500">
|
|
175
|
-
Model:
|
|
176
|
-
<span class="text-gray-300 font-mono"
|
|
176
|
+
Model:
|
|
177
|
+
<span class="text-gray-300 font-mono"
|
|
178
|
+
>${runEntry.model || "—"}</span
|
|
179
|
+
>
|
|
177
180
|
</div>
|
|
178
181
|
<div class="text-gray-500">
|
|
179
|
-
Session:
|
|
180
|
-
<span class="text-gray-300 font-mono"
|
|
182
|
+
Session:
|
|
183
|
+
<span class="text-gray-300 font-mono"
|
|
184
|
+
>${runEntry.sessionKey || "—"}</span
|
|
185
|
+
>
|
|
181
186
|
</div>
|
|
182
187
|
<div class="text-gray-500">
|
|
183
|
-
Tokens in:
|
|
184
|
-
<span class="text-gray-300"
|
|
188
|
+
Tokens in:
|
|
189
|
+
<span class="text-gray-300"
|
|
190
|
+
>${formatTokenCount(runInputTokens)}</span
|
|
191
|
+
>
|
|
185
192
|
</div>
|
|
186
193
|
<div class="text-gray-500">
|
|
187
|
-
Tokens out:
|
|
188
|
-
<span class="text-gray-300"
|
|
194
|
+
Tokens out:
|
|
195
|
+
<span class="text-gray-300"
|
|
196
|
+
>${formatTokenCount(runOutputTokens)}</span
|
|
197
|
+
>
|
|
189
198
|
</div>
|
|
190
199
|
<div class="text-gray-500">
|
|
191
|
-
Total tokens:
|
|
200
|
+
Total tokens:
|
|
192
201
|
<span class="text-gray-300">${formatTokenCount(runTokens)}</span>
|
|
193
202
|
</div>
|
|
194
203
|
<div class="text-gray-500">
|
|
195
|
-
Total cost:
|
|
204
|
+
Total cost:
|
|
196
205
|
<span class="text-gray-300">
|
|
197
|
-
${runEstimatedCost == null
|
|
206
|
+
${runEstimatedCost == null
|
|
207
|
+
? "—"
|
|
208
|
+
: `~${formatCost(runEstimatedCost)}`}
|
|
198
209
|
</span>
|
|
199
210
|
</div>
|
|
200
211
|
</div>
|
|
@@ -236,7 +247,7 @@ export const CronRunHistoryPanel = ({
|
|
|
236
247
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
237
248
|
<div class="flex items-start justify-between gap-3">
|
|
238
249
|
<div class="inline-flex items-center gap-3">
|
|
239
|
-
<h3 class="card-label card-label-bright">
|
|
250
|
+
<h3 class="card-label card-label-bright">Recent runs</h3>
|
|
240
251
|
<div class="text-xs text-gray-500">${entryCountLabel}</div>
|
|
241
252
|
</div>
|
|
242
253
|
<div class="shrink-0 inline-flex items-center gap-2">
|
|
@@ -245,7 +256,8 @@ export const CronRunHistoryPanel = ({
|
|
|
245
256
|
value=${primaryFilterValue}
|
|
246
257
|
onChange=${onChangePrimaryFilter}
|
|
247
258
|
/>
|
|
248
|
-
${Array.isArray(secondaryFilterOptions) &&
|
|
259
|
+
${Array.isArray(secondaryFilterOptions) &&
|
|
260
|
+
secondaryFilterOptions.length > 0
|
|
249
261
|
? html`
|
|
250
262
|
<${SegmentedControl}
|
|
251
263
|
options=${secondaryFilterOptions}
|
|
@@ -288,7 +300,8 @@ export const CronRunHistoryPanel = ({
|
|
|
288
300
|
variant,
|
|
289
301
|
onSelectJob,
|
|
290
302
|
showOpenJobButton,
|
|
291
|
-
})
|
|
303
|
+
}),
|
|
304
|
+
)}
|
|
292
305
|
</div>
|
|
293
306
|
`}
|
|
294
307
|
${footer}
|
|
@@ -6,10 +6,12 @@ import { formatCost } from "./cron-helpers.js";
|
|
|
6
6
|
|
|
7
7
|
const html = htm.bind(h);
|
|
8
8
|
|
|
9
|
+
const kRange24h = "24h";
|
|
9
10
|
const kRange7d = "7d";
|
|
10
11
|
const kRange30d = "30d";
|
|
11
12
|
|
|
12
13
|
const kRanges = [
|
|
14
|
+
{ label: "24h", value: kRange24h },
|
|
13
15
|
{ label: "7d", value: kRange7d },
|
|
14
16
|
{ label: "30d", value: kRange30d },
|
|
15
17
|
];
|
|
@@ -27,6 +29,18 @@ const addLocalDaysMs = (valueMs, dayCount = 0) => {
|
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const getBucketConfig = (range = kRange7d) => {
|
|
32
|
+
if (range === kRange24h) {
|
|
33
|
+
return {
|
|
34
|
+
bucketCount: 24,
|
|
35
|
+
bucketMs: 60 * 60 * 1000,
|
|
36
|
+
formatLabel: (valueMs) =>
|
|
37
|
+
new Date(valueMs).toLocaleTimeString([], {
|
|
38
|
+
hour: "numeric",
|
|
39
|
+
}),
|
|
40
|
+
showLabel: (_, index, total) => index % 3 === 0 || index === total - 1,
|
|
41
|
+
alignToLocalDay: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
30
44
|
if (range === kRange30d) {
|
|
31
45
|
return {
|
|
32
46
|
bucketCount: 30,
|
|
@@ -141,14 +155,18 @@ const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRan
|
|
|
141
155
|
|
|
142
156
|
export const CronRunsTrendCard = ({
|
|
143
157
|
bulkRunsByJobId = {},
|
|
144
|
-
initialRange =
|
|
158
|
+
initialRange = kRange24h,
|
|
145
159
|
selectedBucketFilter = null,
|
|
146
160
|
onBucketFilterChange = () => {},
|
|
147
161
|
}) => {
|
|
148
162
|
const chartCanvasRef = useRef(null);
|
|
149
163
|
const chartInstanceRef = useRef(null);
|
|
150
164
|
const [range, setRange] = useState(
|
|
151
|
-
initialRange === kRange30d
|
|
165
|
+
initialRange === kRange30d
|
|
166
|
+
? kRange30d
|
|
167
|
+
: initialRange === kRange7d
|
|
168
|
+
? kRange7d
|
|
169
|
+
: kRange24h,
|
|
152
170
|
);
|
|
153
171
|
const trend = useMemo(
|
|
154
172
|
() => buildTrendData({ bulkRunsByJobId, nowMs: Date.now(), range }),
|