@runloop/rl-cli 1.8.0 → 1.10.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/README.md +21 -7
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +142 -110
- package/dist/commands/devbox/rsync.js +69 -41
- package/dist/commands/devbox/scp.js +180 -39
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +53 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +18 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +70 -0
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +23 -3
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +26 -62
- package/dist/components/DevboxCreatePage.js +763 -15
- package/dist/components/DevboxDetailPage.js +73 -24
- package/dist/components/GatewayConfigCreatePage.js +272 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +234 -0
- package/dist/components/SecretCreatePage.js +71 -27
- package/dist/components/SettingsMenu.js +12 -2
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +9 -2
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +79 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +6 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
- package/dist/screens/BenchmarkRunListScreen.js +21 -1
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/ScenarioRunDetailScreen.js +6 -0
- package/dist/screens/SecretDetailScreen.js +26 -2
- package/dist/screens/SettingsMenuScreen.js +3 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +47 -0
- package/dist/services/gatewayConfigService.js +153 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +63 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +105 -9
- package/dist/utils/gatewayConfigValidation.js +58 -0
- package/dist/utils/time.js +121 -0
- package/package.json +43 -43
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BenchmarkListScreen - List view for benchmark definitions
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
9
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
10
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
11
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
12
|
+
import { NavigationTips } from "../components/NavigationTips.js";
|
|
13
|
+
import { Table, createTextColumn, createComponentColumn, } from "../components/Table.js";
|
|
14
|
+
import { ActionsPopup } from "../components/ActionsPopup.js";
|
|
15
|
+
import { formatTimeAgo } from "../components/ResourceListView.js";
|
|
16
|
+
import { SearchBar } from "../components/SearchBar.js";
|
|
17
|
+
import { getStatusDisplay } from "../components/StatusBadge.js";
|
|
18
|
+
import { colors } from "../utils/theme.js";
|
|
19
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
20
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
21
|
+
import { useCursorPagination } from "../hooks/useCursorPagination.js";
|
|
22
|
+
import { useListSearch } from "../hooks/useListSearch.js";
|
|
23
|
+
import { listBenchmarks } from "../services/benchmarkService.js";
|
|
24
|
+
export function BenchmarkListScreen() {
|
|
25
|
+
const { exit: inkExit } = useApp();
|
|
26
|
+
const { navigate, goBack } = useNavigation();
|
|
27
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
28
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
29
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
30
|
+
// Search state
|
|
31
|
+
const search = useListSearch({
|
|
32
|
+
onSearchSubmit: () => setSelectedIndex(0),
|
|
33
|
+
onSearchClear: () => setSelectedIndex(0),
|
|
34
|
+
});
|
|
35
|
+
// Calculate overhead for viewport height
|
|
36
|
+
const overhead = 13 + search.getSearchOverhead();
|
|
37
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
38
|
+
overhead,
|
|
39
|
+
minHeight: 5,
|
|
40
|
+
});
|
|
41
|
+
const PAGE_SIZE = viewportHeight;
|
|
42
|
+
// Column widths
|
|
43
|
+
const fixedWidth = 6;
|
|
44
|
+
const idWidth = 25;
|
|
45
|
+
const statusWidth = 12;
|
|
46
|
+
const timeWidth = 18;
|
|
47
|
+
const baseWidth = fixedWidth + idWidth + statusWidth + timeWidth;
|
|
48
|
+
const remainingWidth = terminalWidth - baseWidth;
|
|
49
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
50
|
+
// Fetch function for pagination hook
|
|
51
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
52
|
+
const result = await listBenchmarks({
|
|
53
|
+
limit: params.limit,
|
|
54
|
+
startingAfter: params.startingAt,
|
|
55
|
+
search: search.submittedSearchQuery || undefined,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
items: result.benchmarks,
|
|
59
|
+
hasMore: result.hasMore,
|
|
60
|
+
totalCount: result.totalCount,
|
|
61
|
+
};
|
|
62
|
+
}, [search.submittedSearchQuery]);
|
|
63
|
+
// Use the shared pagination hook
|
|
64
|
+
const { items: benchmarks, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
65
|
+
fetchPage,
|
|
66
|
+
pageSize: PAGE_SIZE,
|
|
67
|
+
getItemId: (benchmark) => benchmark.id,
|
|
68
|
+
pollInterval: 5000,
|
|
69
|
+
pollingEnabled: !showPopup && !search.searchMode,
|
|
70
|
+
deps: [PAGE_SIZE, search.submittedSearchQuery],
|
|
71
|
+
});
|
|
72
|
+
// Operations for benchmarks
|
|
73
|
+
const operations = React.useMemo(() => [
|
|
74
|
+
{
|
|
75
|
+
key: "view_details",
|
|
76
|
+
label: "View Details",
|
|
77
|
+
color: colors.primary,
|
|
78
|
+
icon: figures.pointer,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "create_job",
|
|
82
|
+
label: "Create Benchmark Job",
|
|
83
|
+
color: colors.success,
|
|
84
|
+
icon: figures.play,
|
|
85
|
+
},
|
|
86
|
+
], []);
|
|
87
|
+
// Build columns
|
|
88
|
+
const columns = React.useMemo(() => [
|
|
89
|
+
createTextColumn("id", "ID", (benchmark) => benchmark.id, {
|
|
90
|
+
width: idWidth + 1,
|
|
91
|
+
color: colors.idColor,
|
|
92
|
+
dimColor: false,
|
|
93
|
+
bold: false,
|
|
94
|
+
}),
|
|
95
|
+
createTextColumn("name", "Name", (benchmark) => benchmark.name || "", {
|
|
96
|
+
width: nameWidth,
|
|
97
|
+
}),
|
|
98
|
+
createComponentColumn("status", "Status", (benchmark, _index, isSelected) => {
|
|
99
|
+
const status = benchmark.status || "active";
|
|
100
|
+
const statusDisplay = getStatusDisplay(status);
|
|
101
|
+
const text = statusDisplay.text
|
|
102
|
+
.slice(0, statusWidth)
|
|
103
|
+
.padEnd(statusWidth, " ");
|
|
104
|
+
return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
|
|
105
|
+
}, { width: statusWidth }),
|
|
106
|
+
createTextColumn("created", "Created", (benchmark) => benchmark.created_at
|
|
107
|
+
? formatTimeAgo(benchmark.created_at)
|
|
108
|
+
: "", {
|
|
109
|
+
width: timeWidth,
|
|
110
|
+
color: colors.textDim,
|
|
111
|
+
dimColor: false,
|
|
112
|
+
bold: false,
|
|
113
|
+
}),
|
|
114
|
+
], [idWidth, nameWidth, statusWidth, timeWidth]);
|
|
115
|
+
// Handle Ctrl+C to exit
|
|
116
|
+
useExitOnCtrlC();
|
|
117
|
+
// Ensure selected index is within bounds
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
if (benchmarks.length > 0 && selectedIndex >= benchmarks.length) {
|
|
120
|
+
setSelectedIndex(Math.max(0, benchmarks.length - 1));
|
|
121
|
+
}
|
|
122
|
+
}, [benchmarks.length, selectedIndex]);
|
|
123
|
+
const selectedBenchmark = benchmarks[selectedIndex];
|
|
124
|
+
// Calculate pagination info for display
|
|
125
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
126
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
127
|
+
const endIndex = startIndex + benchmarks.length;
|
|
128
|
+
useInput((input, key) => {
|
|
129
|
+
// Handle search mode input
|
|
130
|
+
if (search.searchMode) {
|
|
131
|
+
if (key.escape) {
|
|
132
|
+
search.cancelSearch();
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Handle popup navigation
|
|
137
|
+
if (showPopup) {
|
|
138
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
139
|
+
setSelectedOperation(selectedOperation - 1);
|
|
140
|
+
}
|
|
141
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
142
|
+
setSelectedOperation(selectedOperation + 1);
|
|
143
|
+
}
|
|
144
|
+
else if (key.return) {
|
|
145
|
+
setShowPopup(false);
|
|
146
|
+
const operationKey = operations[selectedOperation].key;
|
|
147
|
+
if (operationKey === "view_details") {
|
|
148
|
+
navigate("benchmark-detail", {
|
|
149
|
+
benchmarkId: selectedBenchmark.id,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else if (operationKey === "create_job") {
|
|
153
|
+
navigate("benchmark-job-create", {
|
|
154
|
+
initialBenchmarkIds: selectedBenchmark.id,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (input === "v" && selectedBenchmark) {
|
|
159
|
+
setShowPopup(false);
|
|
160
|
+
navigate("benchmark-detail", {
|
|
161
|
+
benchmarkId: selectedBenchmark.id,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else if (input === "c" && selectedBenchmark) {
|
|
165
|
+
setShowPopup(false);
|
|
166
|
+
navigate("benchmark-job-create", {
|
|
167
|
+
initialBenchmarkIds: selectedBenchmark.id,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else if (key.escape || input === "q") {
|
|
171
|
+
setShowPopup(false);
|
|
172
|
+
setSelectedOperation(0);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const pageBenchmarks = benchmarks.length;
|
|
177
|
+
// Handle list view navigation
|
|
178
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
179
|
+
setSelectedIndex(selectedIndex - 1);
|
|
180
|
+
}
|
|
181
|
+
else if (key.downArrow && selectedIndex < pageBenchmarks - 1) {
|
|
182
|
+
setSelectedIndex(selectedIndex + 1);
|
|
183
|
+
}
|
|
184
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
185
|
+
!loading &&
|
|
186
|
+
!navigating &&
|
|
187
|
+
hasMore) {
|
|
188
|
+
nextPage();
|
|
189
|
+
setSelectedIndex(0);
|
|
190
|
+
}
|
|
191
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
192
|
+
!loading &&
|
|
193
|
+
!navigating &&
|
|
194
|
+
hasPrev) {
|
|
195
|
+
prevPage();
|
|
196
|
+
setSelectedIndex(0);
|
|
197
|
+
}
|
|
198
|
+
else if (key.return && selectedBenchmark) {
|
|
199
|
+
navigate("benchmark-detail", {
|
|
200
|
+
benchmarkId: selectedBenchmark.id,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
else if (input === "a" && selectedBenchmark) {
|
|
204
|
+
setShowPopup(true);
|
|
205
|
+
setSelectedOperation(0);
|
|
206
|
+
}
|
|
207
|
+
else if (input === "c" && selectedBenchmark) {
|
|
208
|
+
// Quick shortcut to create a job
|
|
209
|
+
navigate("benchmark-job-create", {
|
|
210
|
+
initialBenchmarkIds: selectedBenchmark.id,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (input === "/") {
|
|
214
|
+
search.enterSearchMode();
|
|
215
|
+
}
|
|
216
|
+
else if (key.escape) {
|
|
217
|
+
if (search.handleEscape()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
goBack();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// Loading state
|
|
224
|
+
if (loading && benchmarks.length === 0) {
|
|
225
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
226
|
+
{ label: "Home" },
|
|
227
|
+
{ label: "Benchmarks" },
|
|
228
|
+
{ label: "Benchmark Definitions", active: true },
|
|
229
|
+
] }), _jsx(SpinnerComponent, { message: "Loading benchmarks..." })] }));
|
|
230
|
+
}
|
|
231
|
+
// Error state
|
|
232
|
+
if (error) {
|
|
233
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
234
|
+
{ label: "Home" },
|
|
235
|
+
{ label: "Benchmarks" },
|
|
236
|
+
{ label: "Benchmark Definitions", active: true },
|
|
237
|
+
] }), _jsx(ErrorMessage, { message: "Failed to list benchmarks", error: error })] }));
|
|
238
|
+
}
|
|
239
|
+
// Main list view
|
|
240
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
241
|
+
{ label: "Home" },
|
|
242
|
+
{ label: "Benchmarks" },
|
|
243
|
+
{ label: "Benchmark Definitions", active: true },
|
|
244
|
+
] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search benchmarks..." }), !showPopup && (_jsx(Table, { data: benchmarks, keyExtractor: (benchmark) => benchmark.id, selectedIndex: selectedIndex, title: `benchmarks[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No benchmarks found"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedBenchmark && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBenchmark, operations: operations.map((op) => ({
|
|
245
|
+
key: op.key,
|
|
246
|
+
label: op.label,
|
|
247
|
+
color: op.color,
|
|
248
|
+
icon: op.icon,
|
|
249
|
+
shortcut: op.key === "view_details"
|
|
250
|
+
? "v"
|
|
251
|
+
: op.key === "create_job"
|
|
252
|
+
? "s"
|
|
253
|
+
: "",
|
|
254
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
255
|
+
{
|
|
256
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
257
|
+
label: "Page",
|
|
258
|
+
condition: hasMore || hasPrev,
|
|
259
|
+
},
|
|
260
|
+
{ key: "Enter", label: "Details" },
|
|
261
|
+
{ key: "c", label: "Create Job" },
|
|
262
|
+
{ key: "a", label: "Actions" },
|
|
263
|
+
{ key: "/", label: "Search" },
|
|
264
|
+
{ key: "Esc", label: "Back" },
|
|
265
|
+
] })] }));
|
|
266
|
+
}
|
|
@@ -5,9 +5,15 @@ export function BenchmarkMenuScreen() {
|
|
|
5
5
|
const { navigate, goBack } = useNavigation();
|
|
6
6
|
const handleSelect = (key) => {
|
|
7
7
|
switch (key) {
|
|
8
|
+
case "benchmarks":
|
|
9
|
+
navigate("benchmark-list");
|
|
10
|
+
break;
|
|
8
11
|
case "benchmark-runs":
|
|
9
12
|
navigate("benchmark-run-list");
|
|
10
13
|
break;
|
|
14
|
+
case "benchmark-jobs":
|
|
15
|
+
navigate("benchmark-job-list");
|
|
16
|
+
break;
|
|
11
17
|
case "scenario-runs":
|
|
12
18
|
navigate("scenario-run-list");
|
|
13
19
|
break;
|
|
@@ -4,15 +4,17 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
* Uses the generic ResourceDetailPage component
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
|
-
import { Text } from "ink";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
8
|
import figures from "figures";
|
|
9
9
|
import { useNavigation } from "../store/navigationStore.js";
|
|
10
10
|
import { useBenchmarkStore, } from "../store/benchmarkStore.js";
|
|
11
11
|
import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
|
|
12
|
-
import { getBenchmarkRun } from "../services/benchmarkService.js";
|
|
12
|
+
import { getBenchmarkRun, listScenarioRuns, } from "../services/benchmarkService.js";
|
|
13
13
|
import { SpinnerComponent } from "../components/Spinner.js";
|
|
14
14
|
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
15
15
|
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
16
|
+
import { getStatusDisplay } from "../components/StatusBadge.js";
|
|
17
|
+
import { Table, createTextColumn, createComponentColumn, } from "../components/Table.js";
|
|
16
18
|
import { colors } from "../utils/theme.js";
|
|
17
19
|
export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
18
20
|
const { goBack, navigate } = useNavigation();
|
|
@@ -20,12 +22,25 @@ export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
|
20
22
|
const [loading, setLoading] = React.useState(false);
|
|
21
23
|
const [error, setError] = React.useState(null);
|
|
22
24
|
const [fetchedRun, setFetchedRun] = React.useState(null);
|
|
25
|
+
const [scenarioRuns, setScenarioRuns] = React.useState([]);
|
|
26
|
+
const [scenarioRunsLoading, setScenarioRunsLoading] = React.useState(false);
|
|
23
27
|
// Find run in store first
|
|
24
28
|
const runFromStore = benchmarkRuns.find((r) => r.id === benchmarkRunId);
|
|
25
29
|
// Polling function
|
|
26
30
|
const pollRun = React.useCallback(async () => {
|
|
27
31
|
if (!benchmarkRunId)
|
|
28
32
|
return null;
|
|
33
|
+
// Also refresh scenario runs when polling
|
|
34
|
+
listScenarioRuns({
|
|
35
|
+
limit: 10,
|
|
36
|
+
benchmarkRunId,
|
|
37
|
+
})
|
|
38
|
+
.then((result) => {
|
|
39
|
+
setScenarioRuns(result.scenarioRuns);
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
// Silently fail for scenario runs
|
|
43
|
+
});
|
|
29
44
|
return getBenchmarkRun(benchmarkRunId);
|
|
30
45
|
}, [benchmarkRunId]);
|
|
31
46
|
// Fetch run from API if not in store
|
|
@@ -44,8 +59,47 @@ export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
|
44
59
|
});
|
|
45
60
|
}
|
|
46
61
|
}, [benchmarkRunId, loading, fetchedRun]);
|
|
62
|
+
// Fetch scenario runs for this benchmark run
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (benchmarkRunId && !scenarioRunsLoading && scenarioRuns.length === 0) {
|
|
65
|
+
setScenarioRunsLoading(true);
|
|
66
|
+
listScenarioRuns({
|
|
67
|
+
limit: 10, // Show up to 10 scenarios
|
|
68
|
+
benchmarkRunId,
|
|
69
|
+
})
|
|
70
|
+
.then((result) => {
|
|
71
|
+
setScenarioRuns(result.scenarioRuns);
|
|
72
|
+
setScenarioRunsLoading(false);
|
|
73
|
+
})
|
|
74
|
+
.catch(() => {
|
|
75
|
+
// Silently fail for scenario runs - not critical
|
|
76
|
+
setScenarioRunsLoading(false);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}, [benchmarkRunId, scenarioRunsLoading, scenarioRuns.length]);
|
|
47
80
|
// Use fetched run for full details, fall back to store for basic display
|
|
48
81
|
const run = fetchedRun || runFromStore;
|
|
82
|
+
// Auto-refresh scenario runs every 5 seconds if benchmark run is running
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
if (!benchmarkRunId || !run)
|
|
85
|
+
return;
|
|
86
|
+
// Only refresh if run is still running
|
|
87
|
+
if (run.state !== "running")
|
|
88
|
+
return;
|
|
89
|
+
const interval = setInterval(() => {
|
|
90
|
+
listScenarioRuns({
|
|
91
|
+
limit: 10,
|
|
92
|
+
benchmarkRunId,
|
|
93
|
+
})
|
|
94
|
+
.then((result) => {
|
|
95
|
+
setScenarioRuns(result.scenarioRuns);
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
// Silently fail
|
|
99
|
+
});
|
|
100
|
+
}, 5000);
|
|
101
|
+
return () => clearInterval(interval);
|
|
102
|
+
}, [benchmarkRunId, run]);
|
|
49
103
|
// Show loading state
|
|
50
104
|
if (!run && benchmarkRunId && !error) {
|
|
51
105
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
@@ -73,12 +127,164 @@ export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
|
73
127
|
{ label: "Not Found", active: true },
|
|
74
128
|
] }), _jsx(ErrorMessage, { message: `Benchmark run ${benchmarkRunId || "unknown"} not found`, error: new Error("Benchmark run not found") })] }));
|
|
75
129
|
}
|
|
130
|
+
// Helper to calculate overall run status based on scenarios
|
|
131
|
+
const calculateOverallStatus = (scenarios) => {
|
|
132
|
+
if (scenarios.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
status: "not-started",
|
|
135
|
+
label: "Not Started",
|
|
136
|
+
color: colors.textDim,
|
|
137
|
+
icon: figures.circle,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Check for any failures or timeouts
|
|
141
|
+
const hasFailed = scenarios.some((s) => s.state === "failed" || s.state === "timeout");
|
|
142
|
+
if (hasFailed) {
|
|
143
|
+
return {
|
|
144
|
+
status: "failed",
|
|
145
|
+
label: "Failed",
|
|
146
|
+
color: colors.error,
|
|
147
|
+
icon: figures.cross,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Check if all are completed
|
|
151
|
+
const allCompleted = scenarios.every((s) => s.state === "completed" || s.state === "scored");
|
|
152
|
+
if (allCompleted) {
|
|
153
|
+
return {
|
|
154
|
+
status: "pass",
|
|
155
|
+
label: "Complete",
|
|
156
|
+
color: colors.success,
|
|
157
|
+
icon: figures.tick,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Check if any are running
|
|
161
|
+
const anyRunning = scenarios.some((s) => s.state === "running" || s.state === "scoring");
|
|
162
|
+
if (anyRunning) {
|
|
163
|
+
return {
|
|
164
|
+
status: "in-progress",
|
|
165
|
+
label: "In Progress",
|
|
166
|
+
color: colors.warning,
|
|
167
|
+
icon: figures.circleFilled,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Default to not started
|
|
171
|
+
return {
|
|
172
|
+
status: "not-started",
|
|
173
|
+
label: "Not Started",
|
|
174
|
+
color: colors.textDim,
|
|
175
|
+
icon: figures.circle,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
// Helper to format duration
|
|
179
|
+
const formatDuration = (ms) => {
|
|
180
|
+
if (ms < 1000)
|
|
181
|
+
return `${ms}ms`;
|
|
182
|
+
const seconds = Math.floor(ms / 1000);
|
|
183
|
+
if (seconds < 60)
|
|
184
|
+
return `${seconds}s`;
|
|
185
|
+
const minutes = Math.floor(seconds / 60);
|
|
186
|
+
const remainingSeconds = seconds % 60;
|
|
187
|
+
if (minutes < 60)
|
|
188
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
189
|
+
const hours = Math.floor(minutes / 60);
|
|
190
|
+
const remainingMinutes = minutes % 60;
|
|
191
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
192
|
+
};
|
|
76
193
|
// Build detail sections
|
|
77
194
|
const detailSections = [];
|
|
78
195
|
// Basic details section
|
|
79
196
|
const basicFields = [];
|
|
80
|
-
if (run.
|
|
197
|
+
if (run.benchmark_id) {
|
|
198
|
+
basicFields.push({
|
|
199
|
+
label: "Benchmark ID",
|
|
200
|
+
value: _jsx(Text, { color: colors.idColor, children: run.benchmark_id }),
|
|
201
|
+
action: {
|
|
202
|
+
type: "navigate",
|
|
203
|
+
screen: "benchmark-detail",
|
|
204
|
+
params: { benchmarkId: run.benchmark_id },
|
|
205
|
+
hint: "View Benchmark",
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (run.purpose) {
|
|
210
|
+
basicFields.push({
|
|
211
|
+
label: "Purpose",
|
|
212
|
+
value: run.purpose,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (run.score !== undefined && run.score !== null) {
|
|
81
216
|
basicFields.push({
|
|
217
|
+
label: "Score",
|
|
218
|
+
value: (_jsx(Text, { color: colors.success, bold: true, children: run.score.toFixed(2) })),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (basicFields.length > 0) {
|
|
222
|
+
detailSections.push({
|
|
223
|
+
title: "Details",
|
|
224
|
+
icon: figures.squareSmallFilled,
|
|
225
|
+
color: colors.warning,
|
|
226
|
+
fields: basicFields,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Overall Status Section
|
|
230
|
+
const overallStatus = calculateOverallStatus(scenarioRuns);
|
|
231
|
+
detailSections.push({
|
|
232
|
+
title: "Overall Status",
|
|
233
|
+
icon: overallStatus.icon,
|
|
234
|
+
color: overallStatus.color,
|
|
235
|
+
fields: [
|
|
236
|
+
{
|
|
237
|
+
label: "Status",
|
|
238
|
+
value: (_jsx(Text, { color: overallStatus.color, bold: true, children: overallStatus.label })),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: "Scenarios",
|
|
242
|
+
value: `${scenarioRuns.length} scenario${scenarioRuns.length !== 1 ? "s" : ""}`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
// Scenario Runs Section
|
|
247
|
+
if (scenarioRuns.length > 0) {
|
|
248
|
+
// Define columns for scenario table
|
|
249
|
+
const scenarioColumns = [
|
|
250
|
+
createTextColumn("id", "ID", (s) => s.id, {
|
|
251
|
+
width: 26,
|
|
252
|
+
color: colors.idColor,
|
|
253
|
+
dimColor: false,
|
|
254
|
+
bold: false,
|
|
255
|
+
}),
|
|
256
|
+
createTextColumn("name", "Name", (s) => s.name || "(unnamed)", {
|
|
257
|
+
width: 50,
|
|
258
|
+
}),
|
|
259
|
+
createComponentColumn("status", "Status", (s, _index, isSelected) => {
|
|
260
|
+
const statusDisplay = getStatusDisplay(s.state);
|
|
261
|
+
const text = statusDisplay.text.slice(0, 12).padEnd(12, " ");
|
|
262
|
+
return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
|
|
263
|
+
}, { width: 12 }),
|
|
264
|
+
createTextColumn("score", "Score", (s) => {
|
|
265
|
+
const score = s.scoring_contract_result?.score;
|
|
266
|
+
return score !== undefined ? String(score) : "";
|
|
267
|
+
}, {
|
|
268
|
+
width: 10,
|
|
269
|
+
color: colors.info,
|
|
270
|
+
}),
|
|
271
|
+
];
|
|
272
|
+
detailSections.push({
|
|
273
|
+
title: "Scenario Runs",
|
|
274
|
+
icon: figures.pointer,
|
|
275
|
+
color: colors.info,
|
|
276
|
+
fields: [
|
|
277
|
+
{
|
|
278
|
+
label: "",
|
|
279
|
+
value: (_jsx(Box, { paddingTop: 1, children: _jsx(Table, { data: scenarioRuns, columns: scenarioColumns, selectedIndex: -1, keyExtractor: (s) => s.id }) })),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Timing section
|
|
285
|
+
const timingFields = [];
|
|
286
|
+
if (run.start_time_ms) {
|
|
287
|
+
timingFields.push({
|
|
82
288
|
label: "Started",
|
|
83
289
|
value: formatTimestamp(run.start_time_ms),
|
|
84
290
|
});
|
|
@@ -87,29 +293,42 @@ export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
|
87
293
|
? run.start_time_ms + run.duration_ms
|
|
88
294
|
: undefined;
|
|
89
295
|
if (endTimeMs) {
|
|
90
|
-
|
|
296
|
+
timingFields.push({
|
|
91
297
|
label: "Ended",
|
|
92
298
|
value: formatTimestamp(endTimeMs),
|
|
93
299
|
});
|
|
94
300
|
}
|
|
95
|
-
if (run.
|
|
96
|
-
|
|
97
|
-
label: "
|
|
98
|
-
value: _jsx(Text, { color: colors.
|
|
301
|
+
if (run.duration_ms) {
|
|
302
|
+
timingFields.push({
|
|
303
|
+
label: "Duration",
|
|
304
|
+
value: _jsx(Text, { color: colors.info, children: formatDuration(run.duration_ms) }),
|
|
99
305
|
});
|
|
100
306
|
}
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
307
|
+
if (timingFields.length > 0) {
|
|
308
|
+
detailSections.push({
|
|
309
|
+
title: "Timing",
|
|
310
|
+
icon: figures.play,
|
|
311
|
+
color: colors.info,
|
|
312
|
+
fields: timingFields,
|
|
105
313
|
});
|
|
106
314
|
}
|
|
107
|
-
|
|
315
|
+
// Secrets Provided section (show keys only, not values)
|
|
316
|
+
if (run.secrets_provided && Object.keys(run.secrets_provided).length > 0) {
|
|
317
|
+
const secretFields = Object.entries(run.secrets_provided).map(([envVar, secretName]) => ({
|
|
318
|
+
label: envVar,
|
|
319
|
+
value: _jsxs(Text, { color: colors.warning, children: [secretName, " (secret)"] }),
|
|
320
|
+
action: {
|
|
321
|
+
type: "navigate",
|
|
322
|
+
screen: "secret-detail",
|
|
323
|
+
params: { secretId: secretName },
|
|
324
|
+
hint: "View Secret",
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
108
327
|
detailSections.push({
|
|
109
|
-
title: "
|
|
110
|
-
icon: figures.
|
|
328
|
+
title: "Secrets Provided",
|
|
329
|
+
icon: figures.warning,
|
|
111
330
|
color: colors.warning,
|
|
112
|
-
fields:
|
|
331
|
+
fields: secretFields,
|
|
113
332
|
});
|
|
114
333
|
}
|
|
115
334
|
// Metadata section
|
|
@@ -154,22 +373,39 @@ export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
|
154
373
|
if (r.benchmark_id) {
|
|
155
374
|
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Benchmark ID: ", r.benchmark_id] }, "core-benchmark"));
|
|
156
375
|
}
|
|
376
|
+
if (r.purpose) {
|
|
377
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Purpose: ", r.purpose] }, "core-purpose"));
|
|
378
|
+
}
|
|
379
|
+
if (r.score !== undefined && r.score !== null) {
|
|
380
|
+
lines.push(_jsxs(Text, { color: colors.success, children: [" ", "Score: ", r.score.toFixed(2)] }, "core-score"));
|
|
381
|
+
}
|
|
382
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
383
|
+
// Timing
|
|
384
|
+
lines.push(_jsx(Text, { color: colors.info, bold: true, children: "Timing" }, "timing-title"));
|
|
157
385
|
if (r.start_time_ms) {
|
|
158
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "
|
|
386
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "timing-started"));
|
|
159
387
|
}
|
|
160
388
|
const detailEndTimeMs = r.start_time_ms && r.duration_ms
|
|
161
389
|
? r.start_time_ms + r.duration_ms
|
|
162
390
|
: undefined;
|
|
163
391
|
if (detailEndTimeMs) {
|
|
164
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "
|
|
392
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "timing-ended"));
|
|
165
393
|
}
|
|
166
|
-
if (r.
|
|
167
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "
|
|
394
|
+
if (r.duration_ms) {
|
|
395
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Duration: ", formatDuration(r.duration_ms)] }, "timing-duration"));
|
|
396
|
+
}
|
|
397
|
+
lines.push(_jsx(Text, { children: " " }, "timing-space"));
|
|
398
|
+
// Secrets Provided
|
|
399
|
+
if (r.secrets_provided && Object.keys(r.secrets_provided).length > 0) {
|
|
400
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Secrets Provided" }, "secrets-title"));
|
|
401
|
+
Object.entries(r.secrets_provided).forEach(([envVar, secretName], idx) => {
|
|
402
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", envVar, " \u2192 ", secretName] }, `secret-${idx}`));
|
|
403
|
+
});
|
|
404
|
+
lines.push(_jsx(Text, { children: " " }, "secrets-space"));
|
|
168
405
|
}
|
|
169
|
-
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
170
406
|
// Metadata
|
|
171
407
|
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
172
|
-
lines.push(_jsx(Text, { color: colors.
|
|
408
|
+
lines.push(_jsx(Text, { color: colors.secondary, bold: true, children: "Metadata" }, "meta-title"));
|
|
173
409
|
Object.entries(r.metadata).forEach(([key, value], idx) => {
|
|
174
410
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
|
|
175
411
|
});
|