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