@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,479 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BenchmarkJobListScreen - List view for benchmark jobs
|
|
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 { listBenchmarkJobs, } from "../services/benchmarkJobService.js";
|
|
24
|
+
export function BenchmarkJobListScreen() {
|
|
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 scoreWidth = 8;
|
|
47
|
+
const statsWidth = 14;
|
|
48
|
+
const timeWidth = 14;
|
|
49
|
+
const baseWidth = fixedWidth + idWidth + statusWidth + scoreWidth + statsWidth + timeWidth;
|
|
50
|
+
const remainingWidth = terminalWidth - baseWidth;
|
|
51
|
+
const nameWidth = Math.min(60, Math.max(15, remainingWidth));
|
|
52
|
+
// Helper to get score from job outcomes
|
|
53
|
+
const getJobScore = (job) => {
|
|
54
|
+
if (!job.benchmark_outcomes || job.benchmark_outcomes.length === 0) {
|
|
55
|
+
return "-";
|
|
56
|
+
}
|
|
57
|
+
// Calculate average score across all outcomes
|
|
58
|
+
const scores = job.benchmark_outcomes
|
|
59
|
+
.map((o) => o.average_score)
|
|
60
|
+
.filter((s) => s !== null && s !== undefined);
|
|
61
|
+
if (scores.length === 0)
|
|
62
|
+
return "-";
|
|
63
|
+
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
64
|
+
return avg.toFixed(2);
|
|
65
|
+
};
|
|
66
|
+
// Helper to get stats from job
|
|
67
|
+
const getJobStats = (job) => {
|
|
68
|
+
if (!job.benchmark_outcomes || job.benchmark_outcomes.length === 0) {
|
|
69
|
+
if (job.in_progress_runs && job.in_progress_runs.length > 0) {
|
|
70
|
+
return `${job.in_progress_runs.length} running`;
|
|
71
|
+
}
|
|
72
|
+
return "-";
|
|
73
|
+
}
|
|
74
|
+
const totalCompleted = job.benchmark_outcomes.reduce((acc, o) => acc + o.n_completed, 0);
|
|
75
|
+
const totalFailed = job.benchmark_outcomes.reduce((acc, o) => acc + o.n_failed, 0);
|
|
76
|
+
const totalTimeout = job.benchmark_outcomes.reduce((acc, o) => acc + o.n_timeout, 0);
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (totalCompleted > 0)
|
|
79
|
+
parts.push(`${totalCompleted}✓`);
|
|
80
|
+
if (totalFailed > 0)
|
|
81
|
+
parts.push(`${totalFailed}✗`);
|
|
82
|
+
if (totalTimeout > 0)
|
|
83
|
+
parts.push(`${totalTimeout}⏱`);
|
|
84
|
+
return parts.length > 0 ? parts.join(" ") : "-";
|
|
85
|
+
};
|
|
86
|
+
// Fetch function for pagination hook
|
|
87
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
88
|
+
const result = await listBenchmarkJobs({
|
|
89
|
+
limit: params.limit,
|
|
90
|
+
startingAfter: params.startingAt,
|
|
91
|
+
name: search.submittedSearchQuery || undefined,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
items: result.jobs,
|
|
95
|
+
hasMore: result.hasMore,
|
|
96
|
+
totalCount: result.totalCount,
|
|
97
|
+
};
|
|
98
|
+
}, [search.submittedSearchQuery]);
|
|
99
|
+
// Use the shared pagination hook
|
|
100
|
+
const { items: benchmarkJobs, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
101
|
+
fetchPage,
|
|
102
|
+
pageSize: PAGE_SIZE,
|
|
103
|
+
getItemId: (job) => job.id,
|
|
104
|
+
pollInterval: 5000,
|
|
105
|
+
pollingEnabled: !showPopup && !search.searchMode,
|
|
106
|
+
deps: [PAGE_SIZE, search.submittedSearchQuery],
|
|
107
|
+
});
|
|
108
|
+
// Operations for benchmark jobs
|
|
109
|
+
const operations = React.useMemo(() => [
|
|
110
|
+
{
|
|
111
|
+
key: "view_details",
|
|
112
|
+
label: "View Details",
|
|
113
|
+
color: colors.primary,
|
|
114
|
+
icon: figures.pointer,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "clone_job",
|
|
118
|
+
label: "Clone Job",
|
|
119
|
+
color: colors.success,
|
|
120
|
+
icon: figures.play,
|
|
121
|
+
},
|
|
122
|
+
], []);
|
|
123
|
+
// Build columns
|
|
124
|
+
const columns = React.useMemo(() => [
|
|
125
|
+
createTextColumn("id", "ID", (job) => job.id, {
|
|
126
|
+
width: idWidth + 1,
|
|
127
|
+
color: colors.idColor,
|
|
128
|
+
dimColor: false,
|
|
129
|
+
bold: false,
|
|
130
|
+
}),
|
|
131
|
+
createTextColumn("name", "Name", (job) => job.name || "", {
|
|
132
|
+
width: nameWidth,
|
|
133
|
+
}),
|
|
134
|
+
createComponentColumn("status", "Status", (job, _index, isSelected) => {
|
|
135
|
+
const statusDisplay = getStatusDisplay(job.state);
|
|
136
|
+
const text = statusDisplay.text
|
|
137
|
+
.slice(0, statusWidth)
|
|
138
|
+
.padEnd(statusWidth, " ");
|
|
139
|
+
return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
|
|
140
|
+
}, { width: statusWidth }),
|
|
141
|
+
createComponentColumn("score", "Score", (job, _index, isSelected) => {
|
|
142
|
+
const score = getJobScore(job);
|
|
143
|
+
const scoreColor = score === "-" ? colors.textDim : colors.success;
|
|
144
|
+
return (_jsx(Text, { color: isSelected ? colors.text : scoreColor, bold: isSelected || score !== "-", inverse: isSelected, children: score.padEnd(scoreWidth, " ") }));
|
|
145
|
+
}, { width: scoreWidth }),
|
|
146
|
+
createTextColumn("stats", "Results", (job) => getJobStats(job), {
|
|
147
|
+
width: statsWidth,
|
|
148
|
+
color: colors.textDim,
|
|
149
|
+
dimColor: false,
|
|
150
|
+
bold: false,
|
|
151
|
+
}),
|
|
152
|
+
createTextColumn("created", "Created", (job) => job.create_time_ms ? formatTimeAgo(job.create_time_ms) : "", {
|
|
153
|
+
width: timeWidth,
|
|
154
|
+
color: colors.textDim,
|
|
155
|
+
dimColor: false,
|
|
156
|
+
bold: false,
|
|
157
|
+
}),
|
|
158
|
+
], [idWidth, nameWidth, statusWidth, scoreWidth, statsWidth, timeWidth]);
|
|
159
|
+
// Handle Ctrl+C to exit
|
|
160
|
+
useExitOnCtrlC();
|
|
161
|
+
// Ensure selected index is within bounds
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
if (benchmarkJobs.length > 0 && selectedIndex >= benchmarkJobs.length) {
|
|
164
|
+
setSelectedIndex(Math.max(0, benchmarkJobs.length - 1));
|
|
165
|
+
}
|
|
166
|
+
}, [benchmarkJobs.length, selectedIndex]);
|
|
167
|
+
const selectedJob = benchmarkJobs[selectedIndex];
|
|
168
|
+
// Calculate pagination info for display
|
|
169
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
170
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
171
|
+
const endIndex = startIndex + benchmarkJobs.length;
|
|
172
|
+
useInput((input, key) => {
|
|
173
|
+
// Handle search mode input
|
|
174
|
+
if (search.searchMode) {
|
|
175
|
+
if (key.escape) {
|
|
176
|
+
search.cancelSearch();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Handle popup navigation
|
|
181
|
+
if (showPopup) {
|
|
182
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
183
|
+
setSelectedOperation(selectedOperation - 1);
|
|
184
|
+
}
|
|
185
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
186
|
+
setSelectedOperation(selectedOperation + 1);
|
|
187
|
+
}
|
|
188
|
+
else if (key.return) {
|
|
189
|
+
setShowPopup(false);
|
|
190
|
+
const operationKey = operations[selectedOperation].key;
|
|
191
|
+
if (operationKey === "view_details" && selectedJob) {
|
|
192
|
+
navigate("benchmark-job-detail", {
|
|
193
|
+
benchmarkJobId: selectedJob.id,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (operationKey === "clone_job" && selectedJob) {
|
|
197
|
+
// Pass job data for cloning
|
|
198
|
+
const cloneParams = {
|
|
199
|
+
cloneFromJobId: selectedJob.id,
|
|
200
|
+
cloneJobName: selectedJob.name,
|
|
201
|
+
};
|
|
202
|
+
// Determine source type and extract IDs
|
|
203
|
+
if (selectedJob.job_spec) {
|
|
204
|
+
const spec = selectedJob.job_spec;
|
|
205
|
+
// Check if it's a scenarios spec (has scenario_ids array)
|
|
206
|
+
if (spec.scenario_ids && Array.isArray(spec.scenario_ids)) {
|
|
207
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
208
|
+
cloneParams.initialScenarioIds = spec.scenario_ids.join(",");
|
|
209
|
+
}
|
|
210
|
+
// Check if it's a benchmark spec (has benchmark_id)
|
|
211
|
+
else if (spec.benchmark_id) {
|
|
212
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
213
|
+
cloneParams.initialBenchmarkIds = spec.benchmark_id;
|
|
214
|
+
}
|
|
215
|
+
// Fallback: check job_source
|
|
216
|
+
else if (selectedJob.job_source) {
|
|
217
|
+
const source = selectedJob.job_source;
|
|
218
|
+
if (source.scenario_ids && Array.isArray(source.scenario_ids)) {
|
|
219
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
220
|
+
cloneParams.initialScenarioIds = source.scenario_ids.join(",");
|
|
221
|
+
}
|
|
222
|
+
else if (source.benchmark_id) {
|
|
223
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
224
|
+
cloneParams.initialBenchmarkIds = source.benchmark_id;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Extract agent configs - both full configs and legacy fields
|
|
229
|
+
if (selectedJob.job_spec?.agent_configs) {
|
|
230
|
+
const agentConfigs = selectedJob.job_spec.agent_configs.map((a) => ({
|
|
231
|
+
agentId: a.agent_id,
|
|
232
|
+
name: a.name,
|
|
233
|
+
modelName: a.model_name,
|
|
234
|
+
timeoutSeconds: a.timeout_seconds,
|
|
235
|
+
kwargs: a.kwargs,
|
|
236
|
+
environmentVariables: a.agent_environment?.environment_variables,
|
|
237
|
+
secrets: a.agent_environment?.secrets,
|
|
238
|
+
}));
|
|
239
|
+
cloneParams.cloneAgentConfigs = JSON.stringify(agentConfigs);
|
|
240
|
+
// Also extract legacy fields for form initialization
|
|
241
|
+
cloneParams.cloneAgentIds = selectedJob.job_spec.agent_configs
|
|
242
|
+
.map((a) => a.agent_id)
|
|
243
|
+
.join(",");
|
|
244
|
+
cloneParams.cloneAgentNames = selectedJob.job_spec.agent_configs
|
|
245
|
+
.map((a) => a.name)
|
|
246
|
+
.join(",");
|
|
247
|
+
}
|
|
248
|
+
// Extract orchestrator config
|
|
249
|
+
if (selectedJob.job_spec?.orchestrator_config) {
|
|
250
|
+
const orch = selectedJob.job_spec.orchestrator_config;
|
|
251
|
+
cloneParams.cloneOrchestratorConfig = JSON.stringify({
|
|
252
|
+
nAttempts: orch.n_attempts,
|
|
253
|
+
nConcurrentTrials: orch.n_concurrent_trials,
|
|
254
|
+
quiet: orch.quiet,
|
|
255
|
+
timeoutMultiplier: orch.timeout_multiplier,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
navigate("benchmark-job-create", cloneParams);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (input === "v" && selectedJob) {
|
|
262
|
+
setShowPopup(false);
|
|
263
|
+
navigate("benchmark-job-detail", {
|
|
264
|
+
benchmarkJobId: selectedJob.id,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else if (input === "n" && selectedJob) {
|
|
268
|
+
setShowPopup(false);
|
|
269
|
+
// Clone the selected job
|
|
270
|
+
const cloneParams = {
|
|
271
|
+
cloneFromJobId: selectedJob.id,
|
|
272
|
+
cloneJobName: selectedJob.name,
|
|
273
|
+
};
|
|
274
|
+
// Determine source type and extract IDs
|
|
275
|
+
if (selectedJob.job_spec) {
|
|
276
|
+
const spec = selectedJob.job_spec;
|
|
277
|
+
// Check if it's a scenarios spec (has scenario_ids array)
|
|
278
|
+
if (spec.scenario_ids && Array.isArray(spec.scenario_ids)) {
|
|
279
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
280
|
+
cloneParams.initialScenarioIds = spec.scenario_ids.join(",");
|
|
281
|
+
}
|
|
282
|
+
// Check if it's a benchmark spec (has benchmark_id)
|
|
283
|
+
else if (spec.benchmark_id) {
|
|
284
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
285
|
+
cloneParams.initialBenchmarkIds = spec.benchmark_id;
|
|
286
|
+
}
|
|
287
|
+
// Fallback: check job_source
|
|
288
|
+
else if (selectedJob.job_source) {
|
|
289
|
+
const source = selectedJob.job_source;
|
|
290
|
+
if (source.scenario_ids && Array.isArray(source.scenario_ids)) {
|
|
291
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
292
|
+
cloneParams.initialScenarioIds = source.scenario_ids.join(",");
|
|
293
|
+
}
|
|
294
|
+
else if (source.benchmark_id) {
|
|
295
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
296
|
+
cloneParams.initialBenchmarkIds = source.benchmark_id;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Extract agent configs - both full configs and legacy fields
|
|
301
|
+
if (selectedJob.job_spec?.agent_configs) {
|
|
302
|
+
const agentConfigs = selectedJob.job_spec.agent_configs.map((a) => ({
|
|
303
|
+
agentId: a.agent_id,
|
|
304
|
+
name: a.name,
|
|
305
|
+
modelName: a.model_name,
|
|
306
|
+
timeoutSeconds: a.timeout_seconds,
|
|
307
|
+
kwargs: a.kwargs,
|
|
308
|
+
environmentVariables: a.agent_environment?.environment_variables,
|
|
309
|
+
secrets: a.agent_environment?.secrets,
|
|
310
|
+
}));
|
|
311
|
+
cloneParams.cloneAgentConfigs = JSON.stringify(agentConfigs);
|
|
312
|
+
// Also extract legacy fields for form initialization
|
|
313
|
+
cloneParams.cloneAgentIds = selectedJob.job_spec.agent_configs
|
|
314
|
+
.map((a) => a.agent_id)
|
|
315
|
+
.join(",");
|
|
316
|
+
cloneParams.cloneAgentNames = selectedJob.job_spec.agent_configs
|
|
317
|
+
.map((a) => a.name)
|
|
318
|
+
.join(",");
|
|
319
|
+
}
|
|
320
|
+
// Extract orchestrator config
|
|
321
|
+
if (selectedJob.job_spec?.orchestrator_config) {
|
|
322
|
+
const orch = selectedJob.job_spec.orchestrator_config;
|
|
323
|
+
cloneParams.cloneOrchestratorConfig = JSON.stringify({
|
|
324
|
+
nAttempts: orch.n_attempts,
|
|
325
|
+
nConcurrentTrials: orch.n_concurrent_trials,
|
|
326
|
+
quiet: orch.quiet,
|
|
327
|
+
timeoutMultiplier: orch.timeout_multiplier,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
navigate("benchmark-job-create", cloneParams);
|
|
331
|
+
}
|
|
332
|
+
else if (key.escape || input === "q") {
|
|
333
|
+
setShowPopup(false);
|
|
334
|
+
setSelectedOperation(0);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const pageJobs = benchmarkJobs.length;
|
|
339
|
+
// Handle list view navigation
|
|
340
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
341
|
+
setSelectedIndex(selectedIndex - 1);
|
|
342
|
+
}
|
|
343
|
+
else if (key.downArrow && selectedIndex < pageJobs - 1) {
|
|
344
|
+
setSelectedIndex(selectedIndex + 1);
|
|
345
|
+
}
|
|
346
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
347
|
+
!loading &&
|
|
348
|
+
!navigating &&
|
|
349
|
+
hasMore) {
|
|
350
|
+
nextPage();
|
|
351
|
+
setSelectedIndex(0);
|
|
352
|
+
}
|
|
353
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
354
|
+
!loading &&
|
|
355
|
+
!navigating &&
|
|
356
|
+
hasPrev) {
|
|
357
|
+
prevPage();
|
|
358
|
+
setSelectedIndex(0);
|
|
359
|
+
}
|
|
360
|
+
else if (key.return && selectedJob) {
|
|
361
|
+
navigate("benchmark-job-detail", {
|
|
362
|
+
benchmarkJobId: selectedJob.id,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
else if (input === "a" && selectedJob) {
|
|
366
|
+
setShowPopup(true);
|
|
367
|
+
setSelectedOperation(0);
|
|
368
|
+
}
|
|
369
|
+
else if (input === "3") {
|
|
370
|
+
// Quick shortcut to clone the selected job, or create a new job if none selected
|
|
371
|
+
if (selectedJob) {
|
|
372
|
+
const cloneParams = {
|
|
373
|
+
cloneFromJobId: selectedJob.id,
|
|
374
|
+
cloneJobName: selectedJob.name,
|
|
375
|
+
};
|
|
376
|
+
// Determine source type and extract IDs
|
|
377
|
+
if (selectedJob.job_spec) {
|
|
378
|
+
const spec = selectedJob.job_spec;
|
|
379
|
+
// Check if it's a scenarios spec (has scenario_ids array)
|
|
380
|
+
if (spec.scenario_ids && Array.isArray(spec.scenario_ids)) {
|
|
381
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
382
|
+
cloneParams.initialScenarioIds = spec.scenario_ids.join(",");
|
|
383
|
+
}
|
|
384
|
+
// Check if it's a benchmark spec (has benchmark_id)
|
|
385
|
+
else if (spec.benchmark_id) {
|
|
386
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
387
|
+
cloneParams.initialBenchmarkIds = spec.benchmark_id;
|
|
388
|
+
}
|
|
389
|
+
// Fallback: check job_source
|
|
390
|
+
else if (selectedJob.job_source) {
|
|
391
|
+
const source = selectedJob.job_source;
|
|
392
|
+
if (source.scenario_ids && Array.isArray(source.scenario_ids)) {
|
|
393
|
+
cloneParams.cloneSourceType = "scenarios";
|
|
394
|
+
cloneParams.initialScenarioIds = source.scenario_ids.join(",");
|
|
395
|
+
}
|
|
396
|
+
else if (source.benchmark_id) {
|
|
397
|
+
cloneParams.cloneSourceType = "benchmark";
|
|
398
|
+
cloneParams.initialBenchmarkIds = source.benchmark_id;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Extract agent configs - both full configs and legacy fields
|
|
403
|
+
if (selectedJob.job_spec?.agent_configs) {
|
|
404
|
+
const agentConfigs = selectedJob.job_spec.agent_configs.map((a) => ({
|
|
405
|
+
agentId: a.agent_id,
|
|
406
|
+
name: a.name,
|
|
407
|
+
modelName: a.model_name,
|
|
408
|
+
timeoutSeconds: a.timeout_seconds,
|
|
409
|
+
kwargs: a.kwargs,
|
|
410
|
+
environmentVariables: a.agent_environment?.environment_variables,
|
|
411
|
+
secrets: a.agent_environment?.secrets,
|
|
412
|
+
}));
|
|
413
|
+
cloneParams.cloneAgentConfigs = JSON.stringify(agentConfigs);
|
|
414
|
+
// Also extract legacy fields for form initialization
|
|
415
|
+
cloneParams.cloneAgentIds = selectedJob.job_spec.agent_configs
|
|
416
|
+
.map((a) => a.agent_id)
|
|
417
|
+
.join(",");
|
|
418
|
+
cloneParams.cloneAgentNames = selectedJob.job_spec.agent_configs
|
|
419
|
+
.map((a) => a.name)
|
|
420
|
+
.join(",");
|
|
421
|
+
}
|
|
422
|
+
// Extract orchestrator config
|
|
423
|
+
if (selectedJob.job_spec?.orchestrator_config) {
|
|
424
|
+
const orch = selectedJob.job_spec.orchestrator_config;
|
|
425
|
+
cloneParams.cloneOrchestratorConfig = JSON.stringify({
|
|
426
|
+
nAttempts: orch.n_attempts,
|
|
427
|
+
nConcurrentTrials: orch.n_concurrent_trials,
|
|
428
|
+
quiet: orch.quiet,
|
|
429
|
+
timeoutMultiplier: orch.timeout_multiplier,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
navigate("benchmark-job-create", cloneParams);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
navigate("benchmark-job-create");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else if (input === "/") {
|
|
439
|
+
search.enterSearchMode();
|
|
440
|
+
}
|
|
441
|
+
else if (key.escape) {
|
|
442
|
+
if (search.handleEscape()) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
goBack();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
// Loading state
|
|
449
|
+
if (loading && benchmarkJobs.length === 0) {
|
|
450
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Home" }, { label: "Benchmark Jobs", active: true }] }), _jsx(SpinnerComponent, { message: "Loading benchmark jobs..." })] }));
|
|
451
|
+
}
|
|
452
|
+
// Error state
|
|
453
|
+
if (error) {
|
|
454
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Home" }, { label: "Benchmark Jobs", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list benchmark jobs", error: error })] }));
|
|
455
|
+
}
|
|
456
|
+
// Main list view
|
|
457
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Home" }, { label: "Benchmark Jobs", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search benchmark jobs..." }), !showPopup && (_jsx(Table, { data: benchmarkJobs, keyExtractor: (job) => job.id, selectedIndex: selectedIndex, title: `benchmark_jobs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No benchmark jobs 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 && selectedJob && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedJob, operations: operations.map((op) => ({
|
|
458
|
+
key: op.key,
|
|
459
|
+
label: op.label,
|
|
460
|
+
color: op.color,
|
|
461
|
+
icon: op.icon,
|
|
462
|
+
shortcut: op.key === "view_details"
|
|
463
|
+
? "v"
|
|
464
|
+
: op.key === "clone_job"
|
|
465
|
+
? "n"
|
|
466
|
+
: "",
|
|
467
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
468
|
+
{
|
|
469
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
470
|
+
label: "Page",
|
|
471
|
+
condition: hasMore || hasPrev,
|
|
472
|
+
},
|
|
473
|
+
{ key: "Enter", label: "Details" },
|
|
474
|
+
{ key: "3", label: "Clone" },
|
|
475
|
+
{ key: "a", label: "Actions" },
|
|
476
|
+
{ key: "/", label: "Search" },
|
|
477
|
+
{ key: "Esc", label: "Back" },
|
|
478
|
+
] })] }));
|
|
479
|
+
}
|