@chrysb/alphaclaw 0.6.2-beta.5 → 0.7.0-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 +37 -13
- package/lib/public/css/cron.css +124 -41
- package/lib/public/css/shell.css +61 -2
- package/lib/public/css/theme.css +2 -1
- package/lib/public/js/app.js +41 -33
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +61 -49
- package/lib/public/js/components/agents-tab/agent-overview/index.js +9 -0
- package/lib/public/js/components/agents-tab/agent-overview/tools-card.js +54 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +297 -203
- package/lib/public/js/components/cron-tab/cron-helpers.js +48 -0
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +294 -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 +74 -62
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +24 -24
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/envars.js +187 -46
- 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/models-tab/index.js +137 -133
- package/lib/public/js/components/models-tab/provider-auth-card.js +8 -1
- package/lib/public/js/components/models-tab/use-models.js +35 -8
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +88 -59
- package/lib/public/js/components/pane-shell.js +27 -0
- package/lib/public/js/components/routes/envars-route.js +1 -3
- package/lib/public/js/components/routes/models-route.js +1 -3
- package/lib/public/js/lib/app-navigation.js +1 -1
- package/lib/server/cost-utils.js +2 -2
- 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
|
+
};
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
formatDurationCompactMs,
|
|
6
6
|
formatLocaleDateTimeWithTodayTime,
|
|
7
7
|
} from "../../lib/format.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatCost,
|
|
10
|
+
formatTokenCount,
|
|
11
|
+
getCronRunEstimatedCost,
|
|
12
|
+
getCronRunTotalTokens,
|
|
13
|
+
} from "./cron-helpers.js";
|
|
9
14
|
|
|
10
15
|
const html = htm.bind(h);
|
|
11
16
|
const runStatusClassName = (status = "") => {
|
|
@@ -17,16 +22,16 @@ const runStatusClassName = (status = "") => {
|
|
|
17
22
|
if (normalized === "skipped") return "text-yellow-300";
|
|
18
23
|
return "text-gray-400";
|
|
19
24
|
};
|
|
20
|
-
const runDeliveryLabel = (run) =>
|
|
21
|
-
|
|
22
|
-
const parsed = Number(runEntry?.estimatedCost);
|
|
23
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
24
|
-
};
|
|
25
|
+
const runDeliveryLabel = (run) =>
|
|
26
|
+
String(run?.deliveryStatus || "not-requested");
|
|
25
27
|
const formatOverviewTimestamp = (timestampMs) =>
|
|
26
28
|
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
27
29
|
fallback: "—",
|
|
28
30
|
valueIsEpochMs: true,
|
|
29
|
-
}).replace(
|
|
31
|
+
}).replace(
|
|
32
|
+
/\s([AP])M\b/g,
|
|
33
|
+
(_, marker) => `${String(marker || "").toLowerCase()}m`,
|
|
34
|
+
);
|
|
30
35
|
const formatDetailTimestamp = (timestampMs) =>
|
|
31
36
|
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
32
37
|
fallback: "—",
|
|
@@ -36,11 +41,7 @@ const formatRowTimestamp = (timestampMs, variant = "overview") =>
|
|
|
36
41
|
variant === "detail"
|
|
37
42
|
? formatDetailTimestamp(timestampMs)
|
|
38
43
|
: formatOverviewTimestamp(timestampMs);
|
|
39
|
-
const renderCollapsedGroupRow = ({
|
|
40
|
-
row,
|
|
41
|
-
rowIndex,
|
|
42
|
-
onSelectJob = () => {},
|
|
43
|
-
}) => {
|
|
44
|
+
const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
44
45
|
const statusSummary = Object.entries(row.statusCounts || {})
|
|
45
46
|
.map(([status, count]) => `${status}: ${count}`)
|
|
46
47
|
.join(" • ");
|
|
@@ -52,17 +53,10 @@ const renderCollapsedGroupRow = ({
|
|
|
52
53
|
>
|
|
53
54
|
<summary class="ac-history-summary">
|
|
54
55
|
<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
|
-
>
|
|
56
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
57
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
63
58
|
<span class="truncate text-xs text-gray-300">
|
|
64
|
-
${row.jobName} -
|
|
65
|
-
${formatTokenCount(row.count)} runs -
|
|
59
|
+
${row.jobName} - ${formatTokenCount(row.count)} runs -
|
|
66
60
|
${timeRangeLabel}
|
|
67
61
|
</span>
|
|
68
62
|
</span>
|
|
@@ -70,12 +64,10 @@ const renderCollapsedGroupRow = ({
|
|
|
70
64
|
</summary>
|
|
71
65
|
<div class="ac-history-body space-y-2 text-xs">
|
|
72
66
|
<div class="text-gray-500">
|
|
73
|
-
${formatTokenCount(row.count)} consecutive runs
|
|
74
|
-
|
|
75
|
-
</div>
|
|
76
|
-
<div class="text-gray-500">
|
|
77
|
-
Statuses: ${statusSummary}
|
|
67
|
+
${formatTokenCount(row.count)} consecutive runs collapsed
|
|
68
|
+
(${timeRangeLabel})
|
|
78
69
|
</div>
|
|
70
|
+
<div class="text-gray-500">Statuses: ${statusSummary}</div>
|
|
79
71
|
${row?.jobId
|
|
80
72
|
? html`
|
|
81
73
|
<div>
|
|
@@ -103,10 +95,14 @@ const renderEntryRow = ({
|
|
|
103
95
|
const runEntry = row?.entry || row || {};
|
|
104
96
|
const runUsage = runEntry?.usage || {};
|
|
105
97
|
const runStatus = String(runEntry?.status || "unknown");
|
|
106
|
-
const runInputTokens = Number(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
98
|
+
const runInputTokens = Number(
|
|
99
|
+
runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0,
|
|
100
|
+
);
|
|
101
|
+
const runOutputTokens = Number(
|
|
102
|
+
runUsage?.output_tokens ?? runUsage?.outputTokens ?? 0,
|
|
103
|
+
);
|
|
104
|
+
const runTokens = getCronRunTotalTokens(runEntry);
|
|
105
|
+
const runEstimatedCost = getCronRunEstimatedCost(runEntry);
|
|
110
106
|
const runTitle = String(runEntry?.jobName || "").trim();
|
|
111
107
|
const hasRunTitle = runTitle.length > 0;
|
|
112
108
|
const isDetail = variant === "detail";
|
|
@@ -117,14 +113,8 @@ const renderEntryRow = ({
|
|
|
117
113
|
>
|
|
118
114
|
<summary class="ac-history-summary">
|
|
119
115
|
<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
|
-
>
|
|
116
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
117
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
128
118
|
${isDetail
|
|
129
119
|
? html`
|
|
130
120
|
<span class="truncate text-xs text-gray-300">
|
|
@@ -134,7 +124,9 @@ const renderEntryRow = ({
|
|
|
134
124
|
: hasRunTitle
|
|
135
125
|
? html`
|
|
136
126
|
<span class="inline-flex items-center gap-2 min-w-0">
|
|
137
|
-
<span class="truncate text-xs text-gray-300"
|
|
127
|
+
<span class="truncate text-xs text-gray-300"
|
|
128
|
+
>${runTitle}</span
|
|
129
|
+
>
|
|
138
130
|
<span class="text-xs text-gray-500 shrink-0">
|
|
139
131
|
${formatRowTimestamp(runEntry.ts, variant)}
|
|
140
132
|
</span>
|
|
@@ -147,17 +139,21 @@ const renderEntryRow = ({
|
|
|
147
139
|
</span>
|
|
148
140
|
`}
|
|
149
141
|
</span>
|
|
150
|
-
<span
|
|
151
|
-
class="inline-flex items-center gap-3 shrink-0 text-xs"
|
|
152
|
-
>
|
|
142
|
+
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
153
143
|
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
154
|
-
<span class="text-gray-400"
|
|
144
|
+
<span class="text-gray-400"
|
|
145
|
+
>${formatDurationCompactMs(runEntry.durationMs)}</span
|
|
146
|
+
>
|
|
155
147
|
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
156
148
|
${isDetail
|
|
157
|
-
? html`<span class="text-gray-500"
|
|
149
|
+
? html`<span class="text-gray-500"
|
|
150
|
+
>${runDeliveryLabel(runEntry)}</span
|
|
151
|
+
>`
|
|
158
152
|
: html`
|
|
159
153
|
<span class="text-gray-500"
|
|
160
|
-
>${runEstimatedCost == null
|
|
154
|
+
>${runEstimatedCost == null
|
|
155
|
+
? "—"
|
|
156
|
+
: `~${formatCost(runEstimatedCost)}`}</span
|
|
161
157
|
>
|
|
162
158
|
`}
|
|
163
159
|
</span>
|
|
@@ -165,36 +161,50 @@ const renderEntryRow = ({
|
|
|
165
161
|
</summary>
|
|
166
162
|
<div class="ac-history-body space-y-2 text-xs">
|
|
167
163
|
${runEntry.summary
|
|
168
|
-
? html`<div
|
|
164
|
+
? html`<div>
|
|
165
|
+
<span class="text-gray-500">Summary:</span> ${runEntry.summary}
|
|
166
|
+
</div>`
|
|
169
167
|
: null}
|
|
170
168
|
${runEntry.error
|
|
171
|
-
? html`<div class="text-red-300"
|
|
169
|
+
? html`<div class="text-red-300">
|
|
170
|
+
<span class="text-gray-500">Error:</span> ${runEntry.error}
|
|
171
|
+
</div>`
|
|
172
172
|
: null}
|
|
173
173
|
<div class="ac-surface-inset rounded-lg p-2.5 space-y-1.5">
|
|
174
174
|
<div class="text-gray-500">
|
|
175
|
-
Model:
|
|
176
|
-
<span class="text-gray-300 font-mono"
|
|
175
|
+
Model:
|
|
176
|
+
<span class="text-gray-300 font-mono"
|
|
177
|
+
>${runEntry.model || "—"}</span
|
|
178
|
+
>
|
|
177
179
|
</div>
|
|
178
180
|
<div class="text-gray-500">
|
|
179
|
-
Session:
|
|
180
|
-
<span class="text-gray-300 font-mono"
|
|
181
|
+
Session:
|
|
182
|
+
<span class="text-gray-300 font-mono"
|
|
183
|
+
>${runEntry.sessionKey || "—"}</span
|
|
184
|
+
>
|
|
181
185
|
</div>
|
|
182
186
|
<div class="text-gray-500">
|
|
183
|
-
Tokens in:
|
|
184
|
-
<span class="text-gray-300"
|
|
187
|
+
Tokens in:
|
|
188
|
+
<span class="text-gray-300"
|
|
189
|
+
>${formatTokenCount(runInputTokens)}</span
|
|
190
|
+
>
|
|
185
191
|
</div>
|
|
186
192
|
<div class="text-gray-500">
|
|
187
|
-
Tokens out:
|
|
188
|
-
<span class="text-gray-300"
|
|
193
|
+
Tokens out:
|
|
194
|
+
<span class="text-gray-300"
|
|
195
|
+
>${formatTokenCount(runOutputTokens)}</span
|
|
196
|
+
>
|
|
189
197
|
</div>
|
|
190
198
|
<div class="text-gray-500">
|
|
191
|
-
Total tokens:
|
|
199
|
+
Total tokens:
|
|
192
200
|
<span class="text-gray-300">${formatTokenCount(runTokens)}</span>
|
|
193
201
|
</div>
|
|
194
202
|
<div class="text-gray-500">
|
|
195
|
-
Total cost:
|
|
203
|
+
Total cost:
|
|
196
204
|
<span class="text-gray-300">
|
|
197
|
-
${runEstimatedCost == null
|
|
205
|
+
${runEstimatedCost == null
|
|
206
|
+
? "—"
|
|
207
|
+
: `~${formatCost(runEstimatedCost)}`}
|
|
198
208
|
</span>
|
|
199
209
|
</div>
|
|
200
210
|
</div>
|
|
@@ -236,7 +246,7 @@ export const CronRunHistoryPanel = ({
|
|
|
236
246
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
237
247
|
<div class="flex items-start justify-between gap-3">
|
|
238
248
|
<div class="inline-flex items-center gap-3">
|
|
239
|
-
<h3 class="card-label card-label-bright">
|
|
249
|
+
<h3 class="card-label card-label-bright">Recent runs</h3>
|
|
240
250
|
<div class="text-xs text-gray-500">${entryCountLabel}</div>
|
|
241
251
|
</div>
|
|
242
252
|
<div class="shrink-0 inline-flex items-center gap-2">
|
|
@@ -245,7 +255,8 @@ export const CronRunHistoryPanel = ({
|
|
|
245
255
|
value=${primaryFilterValue}
|
|
246
256
|
onChange=${onChangePrimaryFilter}
|
|
247
257
|
/>
|
|
248
|
-
${Array.isArray(secondaryFilterOptions) &&
|
|
258
|
+
${Array.isArray(secondaryFilterOptions) &&
|
|
259
|
+
secondaryFilterOptions.length > 0
|
|
249
260
|
? html`
|
|
250
261
|
<${SegmentedControl}
|
|
251
262
|
options=${secondaryFilterOptions}
|
|
@@ -288,7 +299,8 @@ export const CronRunHistoryPanel = ({
|
|
|
288
299
|
variant,
|
|
289
300
|
onSelectJob,
|
|
290
301
|
showOpenJobButton,
|
|
291
|
-
})
|
|
302
|
+
}),
|
|
303
|
+
)}
|
|
292
304
|
</div>
|
|
293
305
|
`}
|
|
294
306
|
${footer}
|
|
@@ -2,14 +2,16 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
-
import { formatCost } from "./cron-helpers.js";
|
|
5
|
+
import { formatCost, getCronRunEstimatedCost } 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,
|
|
@@ -50,25 +64,6 @@ const getBucketConfig = (range = kRange7d) => {
|
|
|
50
64
|
};
|
|
51
65
|
};
|
|
52
66
|
|
|
53
|
-
const getEstimatedCostForEntry = (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
67
|
const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRange7d } = {}) => {
|
|
73
68
|
const config = getBucketConfig(range);
|
|
74
69
|
const safeNowMs = Number.isFinite(Number(nowMs)) ? Number(nowMs) : Date.now();
|
|
@@ -115,7 +110,7 @@ const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRan
|
|
|
115
110
|
if (!Number.isFinite(Number(bucketIndex))) return;
|
|
116
111
|
if (bucketIndex < 0 || bucketIndex >= config.bucketCount) return;
|
|
117
112
|
points[bucketIndex][status] += 1;
|
|
118
|
-
const estimatedCost =
|
|
113
|
+
const estimatedCost = getCronRunEstimatedCost(entry);
|
|
119
114
|
if (estimatedCost != null) {
|
|
120
115
|
points[bucketIndex].totalCost += estimatedCost;
|
|
121
116
|
points[bucketIndex].costCount += 1;
|
|
@@ -141,14 +136,18 @@ const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRan
|
|
|
141
136
|
|
|
142
137
|
export const CronRunsTrendCard = ({
|
|
143
138
|
bulkRunsByJobId = {},
|
|
144
|
-
initialRange =
|
|
139
|
+
initialRange = kRange24h,
|
|
145
140
|
selectedBucketFilter = null,
|
|
146
141
|
onBucketFilterChange = () => {},
|
|
147
142
|
}) => {
|
|
148
143
|
const chartCanvasRef = useRef(null);
|
|
149
144
|
const chartInstanceRef = useRef(null);
|
|
150
145
|
const [range, setRange] = useState(
|
|
151
|
-
initialRange === kRange30d
|
|
146
|
+
initialRange === kRange30d
|
|
147
|
+
? kRange30d
|
|
148
|
+
: initialRange === kRange7d
|
|
149
|
+
? kRange7d
|
|
150
|
+
: kRange24h,
|
|
152
151
|
);
|
|
153
152
|
const trend = useMemo(
|
|
154
153
|
() => buildTrendData({ bulkRunsByJobId, nowMs: Date.now(), range }),
|
|
@@ -287,6 +286,7 @@ export const CronRunsTrendCard = ({
|
|
|
287
286
|
},
|
|
288
287
|
plugins: {
|
|
289
288
|
legend: {
|
|
289
|
+
position: "bottom",
|
|
290
290
|
labels: {
|
|
291
291
|
color: "rgba(209,213,219,1)",
|
|
292
292
|
boxWidth: 10,
|
|
@@ -321,7 +321,7 @@ export const CronRunsTrendCard = ({
|
|
|
321
321
|
return html`
|
|
322
322
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
323
323
|
<div class="flex items-center justify-between gap-2">
|
|
324
|
-
<h3 class="card-label cron-calendar-title">Run
|
|
324
|
+
<h3 class="card-label cron-calendar-title">Run Outcomes</h3>
|
|
325
325
|
<${SegmentedControl}
|
|
326
326
|
options=${kRanges}
|
|
327
327
|
value=${range}
|