@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,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
+ }