@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.
Files changed (62) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +125 -109
  8. package/dist/commands/devbox/tunnel.js +4 -19
  9. package/dist/commands/gateway-config/create.js +44 -0
  10. package/dist/commands/gateway-config/delete.js +21 -0
  11. package/dist/commands/gateway-config/get.js +15 -0
  12. package/dist/commands/gateway-config/list.js +493 -0
  13. package/dist/commands/gateway-config/update.js +60 -0
  14. package/dist/commands/snapshot/list.js +11 -2
  15. package/dist/commands/snapshot/prune.js +265 -0
  16. package/dist/components/BenchmarkMenu.js +23 -3
  17. package/dist/components/DetailedInfoView.js +20 -0
  18. package/dist/components/DevboxActionsMenu.js +9 -61
  19. package/dist/components/DevboxCreatePage.js +531 -14
  20. package/dist/components/DevboxDetailPage.js +27 -22
  21. package/dist/components/GatewayConfigCreatePage.js +265 -0
  22. package/dist/components/LogsViewer.js +6 -40
  23. package/dist/components/ResourceDetailPage.js +143 -160
  24. package/dist/components/ResourceListView.js +3 -33
  25. package/dist/components/ResourcePicker.js +220 -0
  26. package/dist/components/SecretCreatePage.js +2 -4
  27. package/dist/components/SettingsMenu.js +12 -2
  28. package/dist/components/StateHistory.js +1 -20
  29. package/dist/components/StatusBadge.js +9 -2
  30. package/dist/components/StreamingLogsViewer.js +8 -42
  31. package/dist/components/form/FormTextInput.js +4 -2
  32. package/dist/components/resourceDetailTypes.js +18 -0
  33. package/dist/hooks/useInputHandler.js +103 -0
  34. package/dist/router/Router.js +79 -2
  35. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  36. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  37. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  38. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  39. package/dist/screens/BenchmarkListScreen.js +266 -0
  40. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  41. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  42. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  43. package/dist/screens/BlueprintDetailScreen.js +5 -1
  44. package/dist/screens/DevboxCreateScreen.js +2 -2
  45. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  46. package/dist/screens/GatewayConfigListScreen.js +7 -0
  47. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  48. package/dist/screens/SettingsMenuScreen.js +3 -0
  49. package/dist/screens/SnapshotDetailScreen.js +6 -0
  50. package/dist/services/agentService.js +42 -0
  51. package/dist/services/benchmarkJobService.js +122 -0
  52. package/dist/services/benchmarkService.js +47 -0
  53. package/dist/services/gatewayConfigService.js +114 -0
  54. package/dist/services/scenarioService.js +34 -0
  55. package/dist/store/benchmarkJobStore.js +66 -0
  56. package/dist/store/benchmarkStore.js +63 -0
  57. package/dist/store/gatewayConfigStore.js +83 -0
  58. package/dist/utils/browser.js +22 -0
  59. package/dist/utils/clipboard.js +41 -0
  60. package/dist/utils/commands.js +80 -0
  61. package/dist/utils/time.js +121 -0
  62. 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
+ }