@runloop/rl-cli 1.8.0 → 1.9.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 +19 -5
- 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 +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -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 +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -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 +220 -0
- package/dist/components/SecretCreatePage.js +2 -4
- 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/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 +114 -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 +80 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -43
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BenchmarkJobCreateScreen - Create a new benchmark job
|
|
4
|
+
* Uses benchmark definition spec type with multi-select benchmarks and agent picker
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import TextInput from "ink-text-input";
|
|
9
|
+
import figures from "figures";
|
|
10
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
11
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
12
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
13
|
+
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
14
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
15
|
+
import { NavigationTips } from "../components/NavigationTips.js";
|
|
16
|
+
import { ResourcePicker } from "../components/ResourcePicker.js";
|
|
17
|
+
import { colors } from "../utils/theme.js";
|
|
18
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
19
|
+
import { listBenchmarks, getBenchmark } from "../services/benchmarkService.js";
|
|
20
|
+
import { listScenarios, getScenario, } from "../services/scenarioService.js";
|
|
21
|
+
import { listAgents } from "../services/agentService.js";
|
|
22
|
+
import { createBenchmarkJob, } from "../services/benchmarkJobService.js";
|
|
23
|
+
/**
|
|
24
|
+
* Success screen component with input handling
|
|
25
|
+
*/
|
|
26
|
+
function SuccessScreen({ job, onViewDetails, onGoToList, onBack, }) {
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
if (input === "v") {
|
|
29
|
+
onViewDetails();
|
|
30
|
+
}
|
|
31
|
+
else if (input === "l") {
|
|
32
|
+
onGoToList();
|
|
33
|
+
}
|
|
34
|
+
else if (key.escape) {
|
|
35
|
+
onBack();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
39
|
+
{ label: "Home" },
|
|
40
|
+
{ label: "Benchmarks" },
|
|
41
|
+
{ label: "Jobs" },
|
|
42
|
+
{ label: "Created", active: true },
|
|
43
|
+
] }), _jsx(SuccessMessage, { message: "Benchmark job created successfully!" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: job.id })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name:", " "] }), _jsx(Text, { children: job.name })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["State:", " "] }), _jsx(Text, { children: job.state })] })] }), _jsx(NavigationTips, { tips: [
|
|
44
|
+
{ key: "v", label: "View Details" },
|
|
45
|
+
{ key: "l", label: "Jobs List" },
|
|
46
|
+
{ key: "Esc", label: "Back" },
|
|
47
|
+
] })] }));
|
|
48
|
+
}
|
|
49
|
+
export function BenchmarkJobCreateScreen({ initialBenchmarkIds, initialScenarioIds, cloneFromJobId, cloneJobName, cloneSourceType, cloneAgentConfigs, cloneOrchestratorConfig, cloneAgentIds, cloneAgentNames, cloneAgentTimeout, cloneConcurrentTrials, }) {
|
|
50
|
+
const { navigate, goBack } = useNavigation();
|
|
51
|
+
// Determine initial source type and field
|
|
52
|
+
const initialSourceType = cloneSourceType || (initialScenarioIds ? "scenarios" : "benchmark");
|
|
53
|
+
const initialField = initialBenchmarkIds || initialScenarioIds ? "agents" : "source_type";
|
|
54
|
+
const [screenState, setScreenState] = React.useState("form");
|
|
55
|
+
const [currentField, setCurrentField] = React.useState(initialField);
|
|
56
|
+
const [formData, setFormData] = React.useState({
|
|
57
|
+
sourceType: initialSourceType,
|
|
58
|
+
benchmarkId: initialBenchmarkIds || "",
|
|
59
|
+
benchmarkName: "",
|
|
60
|
+
scenarioIds: initialScenarioIds ? initialScenarioIds.split(",") : [],
|
|
61
|
+
scenarioNames: [],
|
|
62
|
+
agentIds: cloneAgentIds ? cloneAgentIds.split(",") : [],
|
|
63
|
+
agentNames: cloneAgentNames ? cloneAgentNames.split(",") : [],
|
|
64
|
+
name: cloneJobName ? `${cloneJobName} (clone)` : "",
|
|
65
|
+
agentTimeout: cloneAgentTimeout || "",
|
|
66
|
+
concurrentTrials: cloneConcurrentTrials || "1",
|
|
67
|
+
});
|
|
68
|
+
const [createdJob, setCreatedJob] = React.useState(null);
|
|
69
|
+
const [error, setError] = React.useState(null);
|
|
70
|
+
// Handle Ctrl+C to exit
|
|
71
|
+
useExitOnCtrlC();
|
|
72
|
+
// Fetch benchmark name if we have an ID (from clone or initial selection)
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
if (initialBenchmarkIds && !formData.benchmarkName) {
|
|
75
|
+
getBenchmark(initialBenchmarkIds)
|
|
76
|
+
.then((benchmark) => {
|
|
77
|
+
setFormData((prev) => ({
|
|
78
|
+
...prev,
|
|
79
|
+
benchmarkName: benchmark.name || benchmark.id,
|
|
80
|
+
}));
|
|
81
|
+
})
|
|
82
|
+
.catch((err) => {
|
|
83
|
+
// Silently fail - user can re-select if needed
|
|
84
|
+
console.error("Failed to fetch benchmark name:", err);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}, [initialBenchmarkIds, formData.benchmarkName]);
|
|
88
|
+
// Fetch scenario names if we have IDs (from clone or initial selection)
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (initialScenarioIds &&
|
|
91
|
+
formData.scenarioIds.length > 0 &&
|
|
92
|
+
formData.scenarioNames.length === 0) {
|
|
93
|
+
// Fetch all scenarios to get their names
|
|
94
|
+
Promise.all(formData.scenarioIds.map((id) => getScenario(id).catch((err) => {
|
|
95
|
+
console.error(`Failed to fetch scenario ${id}:`, err);
|
|
96
|
+
return { id, name: id };
|
|
97
|
+
}))).then((scenarios) => {
|
|
98
|
+
setFormData((prev) => ({
|
|
99
|
+
...prev,
|
|
100
|
+
scenarioNames: scenarios.map((s) => s.name || s.id),
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}, [initialScenarioIds, formData.scenarioIds, formData.scenarioNames]);
|
|
105
|
+
// Field definitions - conditionally include benchmark or scenarios based on source type
|
|
106
|
+
const fields = [
|
|
107
|
+
{
|
|
108
|
+
key: "source_type",
|
|
109
|
+
label: "Source Type",
|
|
110
|
+
type: "toggle",
|
|
111
|
+
required: true,
|
|
112
|
+
description: "Choose between benchmark or scenarios",
|
|
113
|
+
},
|
|
114
|
+
formData.sourceType === "benchmark"
|
|
115
|
+
? {
|
|
116
|
+
key: "benchmark",
|
|
117
|
+
label: "Benchmark",
|
|
118
|
+
type: "picker",
|
|
119
|
+
required: true,
|
|
120
|
+
description: "Select a benchmark definition to run",
|
|
121
|
+
}
|
|
122
|
+
: {
|
|
123
|
+
key: "scenarios",
|
|
124
|
+
label: "Scenarios",
|
|
125
|
+
type: "picker",
|
|
126
|
+
required: true,
|
|
127
|
+
description: "Select one or more scenario definitions to run",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: "agents",
|
|
131
|
+
label: "Agents",
|
|
132
|
+
type: "picker",
|
|
133
|
+
required: true,
|
|
134
|
+
description: "Select one or more agents to run",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "name",
|
|
138
|
+
label: "Job Name",
|
|
139
|
+
type: "text",
|
|
140
|
+
placeholder: "my-benchmark-job",
|
|
141
|
+
description: "Optional name for this job",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "agent_timeout",
|
|
145
|
+
label: "Agent Timeout (s)",
|
|
146
|
+
type: "text",
|
|
147
|
+
placeholder: "300",
|
|
148
|
+
description: "Optional timeout in seconds",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "concurrent_trials",
|
|
152
|
+
label: "Concurrent Trials",
|
|
153
|
+
type: "text",
|
|
154
|
+
placeholder: "1",
|
|
155
|
+
description: "Number of concurrent trials (default: 1)",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: "create",
|
|
159
|
+
label: "Create Benchmark Job",
|
|
160
|
+
type: "action",
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
const fieldKeys = fields.map((f) => f.key);
|
|
164
|
+
const currentFieldIndex = fieldKeys.indexOf(currentField);
|
|
165
|
+
const currentFieldDef = fields.find((f) => f.key === currentField);
|
|
166
|
+
// Check if form is valid
|
|
167
|
+
const isFormValid = ((formData.sourceType === "benchmark" && formData.benchmarkId !== "") ||
|
|
168
|
+
(formData.sourceType === "scenarios" &&
|
|
169
|
+
formData.scenarioIds.length > 0)) &&
|
|
170
|
+
formData.agentIds.length > 0;
|
|
171
|
+
// Memoize the fetchBenchmarksPage function
|
|
172
|
+
const fetchBenchmarksPage = React.useCallback(async (params) => {
|
|
173
|
+
const result = await listBenchmarks({
|
|
174
|
+
limit: params.limit,
|
|
175
|
+
startingAfter: params.startingAt,
|
|
176
|
+
search: params.search,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
items: result.benchmarks,
|
|
180
|
+
hasMore: result.hasMore,
|
|
181
|
+
totalCount: result.totalCount,
|
|
182
|
+
};
|
|
183
|
+
}, []);
|
|
184
|
+
// Memoize the fetchAgentsPage function - fetches all agents
|
|
185
|
+
const fetchAgentsPage = React.useCallback(async (params) => {
|
|
186
|
+
const result = await listAgents({
|
|
187
|
+
limit: params.limit,
|
|
188
|
+
startingAfter: params.startingAt,
|
|
189
|
+
});
|
|
190
|
+
// Apply search filter if provided
|
|
191
|
+
let filteredAgents = result.agents;
|
|
192
|
+
if (params.search) {
|
|
193
|
+
const searchLower = params.search.toLowerCase();
|
|
194
|
+
filteredAgents = result.agents.filter((agent) => agent.name.toLowerCase().includes(searchLower) ||
|
|
195
|
+
agent.id.toLowerCase().includes(searchLower));
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
items: filteredAgents,
|
|
199
|
+
hasMore: result.hasMore,
|
|
200
|
+
totalCount: filteredAgents.length,
|
|
201
|
+
};
|
|
202
|
+
}, []);
|
|
203
|
+
// Memoize the fetchScenariosPage function
|
|
204
|
+
const fetchScenariosPage = React.useCallback(async (params) => {
|
|
205
|
+
const result = await listScenarios({
|
|
206
|
+
limit: params.limit,
|
|
207
|
+
startingAfter: params.startingAt,
|
|
208
|
+
search: params.search,
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
items: result.scenarios,
|
|
212
|
+
hasMore: result.hasMore,
|
|
213
|
+
totalCount: result.totalCount,
|
|
214
|
+
};
|
|
215
|
+
}, []);
|
|
216
|
+
// Memoize benchmark picker config (single-select)
|
|
217
|
+
const benchmarkPickerConfig = React.useMemo(() => ({
|
|
218
|
+
title: "Select Benchmark",
|
|
219
|
+
fetchPage: fetchBenchmarksPage,
|
|
220
|
+
getItemId: (benchmark) => benchmark.id,
|
|
221
|
+
getItemLabel: (benchmark) => benchmark.name || benchmark.id,
|
|
222
|
+
getItemStatus: (benchmark) => benchmark.status,
|
|
223
|
+
mode: "single",
|
|
224
|
+
minSelection: 1,
|
|
225
|
+
emptyMessage: "No benchmarks found",
|
|
226
|
+
searchPlaceholder: "Search benchmarks...",
|
|
227
|
+
breadcrumbItems: [
|
|
228
|
+
{ label: "Home" },
|
|
229
|
+
{ label: "Benchmarks" },
|
|
230
|
+
{ label: "Jobs" },
|
|
231
|
+
{ label: "Create" },
|
|
232
|
+
{ label: "Select Benchmark", active: true },
|
|
233
|
+
],
|
|
234
|
+
}), [fetchBenchmarksPage]);
|
|
235
|
+
// Memoize scenario picker config (multi-select)
|
|
236
|
+
const scenarioPickerConfig = React.useMemo(() => ({
|
|
237
|
+
title: "Select Scenarios",
|
|
238
|
+
fetchPage: fetchScenariosPage,
|
|
239
|
+
getItemId: (scenario) => scenario.id,
|
|
240
|
+
getItemLabel: (scenario) => scenario.name || scenario.id,
|
|
241
|
+
getItemStatus: (scenario) => scenario.is_public ? "public" : "private",
|
|
242
|
+
mode: "multi",
|
|
243
|
+
minSelection: 1,
|
|
244
|
+
emptyMessage: "No scenarios found",
|
|
245
|
+
searchPlaceholder: "Search scenarios...",
|
|
246
|
+
breadcrumbItems: [
|
|
247
|
+
{ label: "Home" },
|
|
248
|
+
{ label: "Benchmarks" },
|
|
249
|
+
{ label: "Jobs" },
|
|
250
|
+
{ label: "Create" },
|
|
251
|
+
{ label: "Select Scenarios", active: true },
|
|
252
|
+
],
|
|
253
|
+
}), [fetchScenariosPage]);
|
|
254
|
+
// Memoize agent picker config (multi-select)
|
|
255
|
+
const agentPickerConfig = React.useMemo(() => ({
|
|
256
|
+
title: "Select Agents",
|
|
257
|
+
fetchPage: fetchAgentsPage,
|
|
258
|
+
getItemId: (agent) => agent.id,
|
|
259
|
+
getItemLabel: (agent) => agent.name,
|
|
260
|
+
getItemStatus: (agent) => (agent.is_public ? "public" : "private"),
|
|
261
|
+
mode: "multi",
|
|
262
|
+
minSelection: 1,
|
|
263
|
+
emptyMessage: "No agents found",
|
|
264
|
+
searchPlaceholder: "Search agents...",
|
|
265
|
+
breadcrumbItems: [
|
|
266
|
+
{ label: "Home" },
|
|
267
|
+
{ label: "Benchmarks" },
|
|
268
|
+
{ label: "Jobs" },
|
|
269
|
+
{ label: "Create" },
|
|
270
|
+
{ label: "Select Agents", active: true },
|
|
271
|
+
],
|
|
272
|
+
}), [fetchAgentsPage]);
|
|
273
|
+
// Handle benchmark selection (single)
|
|
274
|
+
const handleBenchmarkSelect = React.useCallback((items) => {
|
|
275
|
+
if (items.length > 0) {
|
|
276
|
+
const benchmark = items[0];
|
|
277
|
+
setFormData((prev) => ({
|
|
278
|
+
...prev,
|
|
279
|
+
benchmarkId: benchmark.id,
|
|
280
|
+
benchmarkName: benchmark.name || benchmark.id,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
setScreenState("form");
|
|
284
|
+
}, []);
|
|
285
|
+
// Handle scenario selection (multi)
|
|
286
|
+
const handleScenarioSelect = React.useCallback((items) => {
|
|
287
|
+
setFormData((prev) => ({
|
|
288
|
+
...prev,
|
|
289
|
+
scenarioIds: items.map((s) => s.id),
|
|
290
|
+
scenarioNames: items.map((s) => s.name || s.id),
|
|
291
|
+
}));
|
|
292
|
+
setScreenState("form");
|
|
293
|
+
}, []);
|
|
294
|
+
// Handle agent selection (multi)
|
|
295
|
+
const handleAgentSelect = React.useCallback((items) => {
|
|
296
|
+
setFormData((prev) => ({
|
|
297
|
+
...prev,
|
|
298
|
+
agentIds: items.map((a) => a.id),
|
|
299
|
+
agentNames: items.map((a) => a.name),
|
|
300
|
+
}));
|
|
301
|
+
setScreenState("form");
|
|
302
|
+
}, []);
|
|
303
|
+
// Handle create
|
|
304
|
+
const handleCreate = React.useCallback(async () => {
|
|
305
|
+
if (!isFormValid)
|
|
306
|
+
return;
|
|
307
|
+
setScreenState("creating");
|
|
308
|
+
setError(null);
|
|
309
|
+
try {
|
|
310
|
+
// Use cloned agent configs if available, otherwise build from form
|
|
311
|
+
let agentConfigs;
|
|
312
|
+
if (cloneAgentConfigs) {
|
|
313
|
+
// Use the full cloned configs
|
|
314
|
+
agentConfigs = JSON.parse(cloneAgentConfigs);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Build agent configs from form data (backward compatibility)
|
|
318
|
+
agentConfigs = formData.agentIds.map((agentId, index) => {
|
|
319
|
+
const config = {
|
|
320
|
+
name: formData.agentNames[index],
|
|
321
|
+
agentId: agentId,
|
|
322
|
+
};
|
|
323
|
+
if (formData.agentTimeout) {
|
|
324
|
+
const timeout = parseInt(formData.agentTimeout, 10);
|
|
325
|
+
if (!isNaN(timeout) && timeout > 0) {
|
|
326
|
+
config.timeoutSeconds = timeout;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return config;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// Use cloned orchestrator config if available, otherwise build from form
|
|
333
|
+
let orchestratorConfig;
|
|
334
|
+
if (cloneOrchestratorConfig) {
|
|
335
|
+
orchestratorConfig = JSON.parse(cloneOrchestratorConfig);
|
|
336
|
+
}
|
|
337
|
+
else if (formData.concurrentTrials) {
|
|
338
|
+
orchestratorConfig = {
|
|
339
|
+
nConcurrentTrials: parseInt(formData.concurrentTrials, 10) || 1,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const job = await createBenchmarkJob({
|
|
343
|
+
name: formData.name || undefined,
|
|
344
|
+
benchmarkId: formData.sourceType === "benchmark"
|
|
345
|
+
? formData.benchmarkId
|
|
346
|
+
: undefined,
|
|
347
|
+
scenarioIds: formData.sourceType === "scenarios"
|
|
348
|
+
? formData.scenarioIds
|
|
349
|
+
: undefined,
|
|
350
|
+
agentConfigs,
|
|
351
|
+
orchestratorConfig,
|
|
352
|
+
});
|
|
353
|
+
setCreatedJob(job);
|
|
354
|
+
setScreenState("success");
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
setError(err);
|
|
358
|
+
setScreenState("error");
|
|
359
|
+
}
|
|
360
|
+
}, [formData, isFormValid, cloneAgentConfigs, cloneOrchestratorConfig]);
|
|
361
|
+
// Handle input
|
|
362
|
+
useInput((input, key) => {
|
|
363
|
+
if (screenState !== "form")
|
|
364
|
+
return;
|
|
365
|
+
// Handle source type toggle with left/right arrows
|
|
366
|
+
if (currentField === "source_type" && (key.leftArrow || key.rightArrow)) {
|
|
367
|
+
setFormData((prev) => ({
|
|
368
|
+
...prev,
|
|
369
|
+
sourceType: prev.sourceType === "benchmark" ? "scenarios" : "benchmark",
|
|
370
|
+
// Clear the other source when switching
|
|
371
|
+
benchmarkId: prev.sourceType === "scenarios" ? "" : prev.benchmarkId,
|
|
372
|
+
benchmarkName: prev.sourceType === "scenarios" ? "" : prev.benchmarkName,
|
|
373
|
+
scenarioIds: prev.sourceType === "benchmark" ? [] : prev.scenarioIds,
|
|
374
|
+
scenarioNames: prev.sourceType === "benchmark" ? [] : prev.scenarioNames,
|
|
375
|
+
}));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Navigate between fields
|
|
379
|
+
if (key.upArrow && currentFieldIndex > 0) {
|
|
380
|
+
setCurrentField(fieldKeys[currentFieldIndex - 1]);
|
|
381
|
+
}
|
|
382
|
+
else if (key.downArrow && currentFieldIndex < fieldKeys.length - 1) {
|
|
383
|
+
setCurrentField(fieldKeys[currentFieldIndex + 1]);
|
|
384
|
+
}
|
|
385
|
+
else if (key.escape) {
|
|
386
|
+
goBack();
|
|
387
|
+
}
|
|
388
|
+
else if (key.return) {
|
|
389
|
+
if (currentFieldDef?.type === "picker" && currentField === "benchmark") {
|
|
390
|
+
setScreenState("picking_benchmark");
|
|
391
|
+
}
|
|
392
|
+
else if (currentFieldDef?.type === "picker" &&
|
|
393
|
+
currentField === "scenarios") {
|
|
394
|
+
setScreenState("picking_scenarios");
|
|
395
|
+
}
|
|
396
|
+
else if (currentFieldDef?.type === "picker" &&
|
|
397
|
+
currentField === "agents") {
|
|
398
|
+
setScreenState("picking_agents");
|
|
399
|
+
}
|
|
400
|
+
else if (currentFieldDef?.type === "action" &&
|
|
401
|
+
currentField === "create") {
|
|
402
|
+
handleCreate();
|
|
403
|
+
}
|
|
404
|
+
else if (currentFieldIndex < fieldKeys.length - 1) {
|
|
405
|
+
// Move to next field on Enter for text inputs
|
|
406
|
+
setCurrentField(fieldKeys[currentFieldIndex + 1]);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// Show benchmark picker (single-select)
|
|
411
|
+
if (screenState === "picking_benchmark") {
|
|
412
|
+
return (_jsx(ResourcePicker, { config: benchmarkPickerConfig, onSelect: handleBenchmarkSelect, onCancel: () => setScreenState("form"), initialSelected: formData.benchmarkId ? [formData.benchmarkId] : [] }));
|
|
413
|
+
}
|
|
414
|
+
// Show scenario picker (multi-select)
|
|
415
|
+
if (screenState === "picking_scenarios") {
|
|
416
|
+
return (_jsx(ResourcePicker, { config: scenarioPickerConfig, onSelect: handleScenarioSelect, onCancel: () => setScreenState("form"), initialSelected: formData.scenarioIds }));
|
|
417
|
+
}
|
|
418
|
+
// Show agent picker (multi-select)
|
|
419
|
+
if (screenState === "picking_agents") {
|
|
420
|
+
return (_jsx(ResourcePicker, { config: agentPickerConfig, onSelect: handleAgentSelect, onCancel: () => setScreenState("form"), initialSelected: formData.agentIds }));
|
|
421
|
+
}
|
|
422
|
+
// Show creating state
|
|
423
|
+
if (screenState === "creating") {
|
|
424
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
425
|
+
{ label: "Home" },
|
|
426
|
+
{ label: "Benchmarks" },
|
|
427
|
+
{ label: "Jobs" },
|
|
428
|
+
{ label: "Create", active: true },
|
|
429
|
+
] }), _jsx(SpinnerComponent, { message: "Creating benchmark job..." })] }));
|
|
430
|
+
}
|
|
431
|
+
// Show success state
|
|
432
|
+
if (screenState === "success" && createdJob) {
|
|
433
|
+
return (_jsx(SuccessScreen, { job: createdJob, onViewDetails: () => navigate("benchmark-job-detail", { benchmarkJobId: createdJob.id }), onGoToList: () => navigate("benchmark-job-list"), onBack: goBack }));
|
|
434
|
+
}
|
|
435
|
+
// Show error state
|
|
436
|
+
if (screenState === "error") {
|
|
437
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
438
|
+
{ label: "Home" },
|
|
439
|
+
{ label: "Benchmarks" },
|
|
440
|
+
{ label: "Jobs" },
|
|
441
|
+
{ label: "Error", active: true },
|
|
442
|
+
] }), _jsx(ErrorMessage, { message: "Failed to create benchmark job", error: error || new Error("Unknown error") }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["Press ", _jsx(Text, { color: colors.primary, children: "r" }), " to retry or", " ", _jsx(Text, { color: colors.primary, children: "Esc" }), " to go back"] }) }), _jsx(NavigationTips, { tips: [
|
|
443
|
+
{ key: "r", label: "Retry" },
|
|
444
|
+
{ key: "Esc", label: "Back" },
|
|
445
|
+
] })] }));
|
|
446
|
+
}
|
|
447
|
+
// Helper to get display value for a field
|
|
448
|
+
const getFieldValue = (fieldKey) => {
|
|
449
|
+
switch (fieldKey) {
|
|
450
|
+
case "source_type":
|
|
451
|
+
return formData.sourceType === "benchmark" ? "Benchmark" : "Scenarios";
|
|
452
|
+
case "benchmark":
|
|
453
|
+
return formData.benchmarkName;
|
|
454
|
+
case "scenarios":
|
|
455
|
+
// Show count based on IDs even if names aren't loaded yet
|
|
456
|
+
if (formData.scenarioIds.length === 0)
|
|
457
|
+
return "";
|
|
458
|
+
if (formData.scenarioIds.length === 1) {
|
|
459
|
+
return formData.scenarioNames[0] || formData.scenarioIds[0];
|
|
460
|
+
}
|
|
461
|
+
// If we have names, show the first name + count, otherwise show count
|
|
462
|
+
if (formData.scenarioNames.length > 0) {
|
|
463
|
+
return `${formData.scenarioNames.length} scenarios selected`;
|
|
464
|
+
}
|
|
465
|
+
return `${formData.scenarioIds.length} scenarios selected`;
|
|
466
|
+
case "agents":
|
|
467
|
+
if (formData.agentNames.length === 0)
|
|
468
|
+
return "";
|
|
469
|
+
if (formData.agentNames.length === 1)
|
|
470
|
+
return formData.agentNames[0];
|
|
471
|
+
return `${formData.agentNames.length} agents selected`;
|
|
472
|
+
case "name":
|
|
473
|
+
return formData.name;
|
|
474
|
+
case "agent_timeout":
|
|
475
|
+
return formData.agentTimeout;
|
|
476
|
+
case "concurrent_trials":
|
|
477
|
+
return formData.concurrentTrials;
|
|
478
|
+
default:
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
// Main form view
|
|
483
|
+
const isCloning = !!cloneFromJobId;
|
|
484
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
485
|
+
{ label: "Home" },
|
|
486
|
+
{ label: "Benchmarks" },
|
|
487
|
+
{ label: "Jobs" },
|
|
488
|
+
{ label: isCloning ? "Clone Job" : "Create", active: true },
|
|
489
|
+
] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " ", isCloning ? "Clone Benchmark Job" : "Create Benchmark Job"] }) }), fields.map((field) => {
|
|
490
|
+
const isSelected = currentField === field.key;
|
|
491
|
+
const value = getFieldValue(field.key);
|
|
492
|
+
return (_jsxs(Box, { marginBottom: field.type === "action" ? 1 : 0, children: [_jsx(Box, { width: 4, children: _jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }) }), field.type === "toggle" ? (_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [field.label, field.required && _jsx(Text, { color: colors.error, children: "*" }), ":", " "] }), _jsxs(Text, { color: isSelected ? colors.text : colors.textDim, children: [isSelected ? figures.arrowLeft : "", " "] }), _jsx(Text, { color: formData.sourceType === "benchmark"
|
|
493
|
+
? colors.primary
|
|
494
|
+
: colors.textDim, bold: formData.sourceType === "benchmark", children: "Benchmark" }), _jsx(Text, { color: colors.textDim, children: " / " }), _jsx(Text, { color: formData.sourceType === "scenarios"
|
|
495
|
+
? colors.primary
|
|
496
|
+
: colors.textDim, bold: formData.sourceType === "scenarios", children: "Scenarios" }), _jsxs(Text, { color: isSelected ? colors.text : colors.textDim, children: [" ", isSelected ? figures.arrowRight : ""] })] })) : field.type === "action" ? (_jsx(Box, { children: _jsxs(Text, { color: isFormValid ? colors.success : colors.textDim, bold: isSelected, inverse: isSelected, children: [" ", figures.play, " ", field.label, " ", !isFormValid && "(select benchmark/scenarios and agents)"] }) })) : field.type === "picker" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [field.label, field.required && _jsx(Text, { color: colors.error, children: "*" }), ":", " "] }), value ? (_jsx(Text, { color: colors.idColor, children: value })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" }))] }), isSelected && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Enter to select" }) }))] })) : (_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [field.label, field.required && _jsx(Text, { color: colors.error, children: "*" }), ":", " "] }), isSelected ? (_jsx(TextInput, { value: value, onChange: (val) => {
|
|
497
|
+
if (field.key === "name") {
|
|
498
|
+
setFormData((prev) => ({ ...prev, name: val }));
|
|
499
|
+
}
|
|
500
|
+
else if (field.key === "agent_timeout") {
|
|
501
|
+
setFormData((prev) => ({
|
|
502
|
+
...prev,
|
|
503
|
+
agentTimeout: val,
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
else if (field.key === "concurrent_trials") {
|
|
507
|
+
setFormData((prev) => ({
|
|
508
|
+
...prev,
|
|
509
|
+
concurrentTrials: val,
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
}, placeholder: field.placeholder })) : (_jsx(Text, { color: value ? colors.text : colors.textDim, children: value || field.placeholder || "(empty)" }))] }))] }, field.key));
|
|
513
|
+
}), currentFieldDef?.description && (_jsx(Box, { marginTop: 1, marginLeft: 4, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " ", currentFieldDef.description] }) }))] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
514
|
+
{
|
|
515
|
+
key: "Enter",
|
|
516
|
+
label: currentFieldDef?.type === "picker"
|
|
517
|
+
? "Select"
|
|
518
|
+
: currentFieldDef?.type === "action"
|
|
519
|
+
? "Create"
|
|
520
|
+
: "Next",
|
|
521
|
+
},
|
|
522
|
+
{ key: "Esc", label: "Back" },
|
|
523
|
+
] })] }));
|
|
524
|
+
}
|