@runloop/rl-cli 1.7.0 → 1.8.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.
@@ -0,0 +1,255 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * BenchmarkRunListScreen - List view for benchmark runs
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 { listBenchmarkRuns } from "../services/benchmarkService.js";
24
+ export function BenchmarkRunListScreen() {
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 listBenchmarkRuns({
53
+ limit: params.limit,
54
+ startingAfter: params.startingAt,
55
+ });
56
+ return {
57
+ items: result.benchmarkRuns,
58
+ hasMore: result.hasMore,
59
+ totalCount: result.totalCount,
60
+ };
61
+ }, []);
62
+ // Use the shared pagination hook
63
+ const { items: benchmarkRuns, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
64
+ fetchPage,
65
+ pageSize: PAGE_SIZE,
66
+ getItemId: (run) => run.id,
67
+ pollInterval: 5000,
68
+ pollingEnabled: !showPopup && !search.searchMode,
69
+ deps: [PAGE_SIZE],
70
+ });
71
+ // Operations for benchmark runs
72
+ const operations = React.useMemo(() => [
73
+ {
74
+ key: "view_details",
75
+ label: "View Details",
76
+ color: colors.primary,
77
+ icon: figures.pointer,
78
+ },
79
+ {
80
+ key: "view_scenarios",
81
+ label: "View Scenario Runs",
82
+ color: colors.info,
83
+ icon: figures.arrowRight,
84
+ },
85
+ ], []);
86
+ // Build columns
87
+ const columns = React.useMemo(() => [
88
+ createTextColumn("id", "ID", (run) => run.id, {
89
+ width: idWidth + 1,
90
+ color: colors.idColor,
91
+ dimColor: false,
92
+ bold: false,
93
+ }),
94
+ createTextColumn("name", "Name", (run) => run.name || "", {
95
+ width: nameWidth,
96
+ }),
97
+ createComponentColumn("status", "Status", (run, _index, isSelected) => {
98
+ const statusDisplay = getStatusDisplay(run.state);
99
+ const text = statusDisplay.text
100
+ .slice(0, statusWidth)
101
+ .padEnd(statusWidth, " ");
102
+ return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
103
+ }, { width: statusWidth }),
104
+ createTextColumn("created", "Created", (run) => run.start_time_ms ? formatTimeAgo(run.start_time_ms) : "", {
105
+ width: timeWidth,
106
+ color: colors.textDim,
107
+ dimColor: false,
108
+ bold: false,
109
+ }),
110
+ ], [idWidth, nameWidth, statusWidth, timeWidth]);
111
+ // Handle Ctrl+C to exit
112
+ useExitOnCtrlC();
113
+ // Ensure selected index is within bounds
114
+ React.useEffect(() => {
115
+ if (benchmarkRuns.length > 0 && selectedIndex >= benchmarkRuns.length) {
116
+ setSelectedIndex(Math.max(0, benchmarkRuns.length - 1));
117
+ }
118
+ }, [benchmarkRuns.length, selectedIndex]);
119
+ const selectedRun = benchmarkRuns[selectedIndex];
120
+ // Calculate pagination info for display
121
+ const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
122
+ const startIndex = currentPage * PAGE_SIZE;
123
+ const endIndex = startIndex + benchmarkRuns.length;
124
+ useInput((input, key) => {
125
+ // Handle search mode input
126
+ if (search.searchMode) {
127
+ if (key.escape) {
128
+ search.cancelSearch();
129
+ }
130
+ return;
131
+ }
132
+ // Handle popup navigation
133
+ if (showPopup) {
134
+ if (key.upArrow && selectedOperation > 0) {
135
+ setSelectedOperation(selectedOperation - 1);
136
+ }
137
+ else if (key.downArrow && selectedOperation < operations.length - 1) {
138
+ setSelectedOperation(selectedOperation + 1);
139
+ }
140
+ else if (key.return) {
141
+ setShowPopup(false);
142
+ const operationKey = operations[selectedOperation].key;
143
+ if (operationKey === "view_details") {
144
+ navigate("benchmark-run-detail", {
145
+ benchmarkRunId: selectedRun.id,
146
+ });
147
+ }
148
+ else if (operationKey === "view_scenarios") {
149
+ navigate("scenario-run-list", {
150
+ benchmarkRunId: selectedRun.id,
151
+ });
152
+ }
153
+ }
154
+ else if (input === "v" && selectedRun) {
155
+ setShowPopup(false);
156
+ navigate("benchmark-run-detail", {
157
+ benchmarkRunId: selectedRun.id,
158
+ });
159
+ }
160
+ else if (input === "s" && selectedRun) {
161
+ setShowPopup(false);
162
+ navigate("scenario-run-list", {
163
+ benchmarkRunId: selectedRun.id,
164
+ });
165
+ }
166
+ else if (key.escape || input === "q") {
167
+ setShowPopup(false);
168
+ setSelectedOperation(0);
169
+ }
170
+ return;
171
+ }
172
+ const pageRuns = benchmarkRuns.length;
173
+ // Handle list view navigation
174
+ if (key.upArrow && selectedIndex > 0) {
175
+ setSelectedIndex(selectedIndex - 1);
176
+ }
177
+ else if (key.downArrow && selectedIndex < pageRuns - 1) {
178
+ setSelectedIndex(selectedIndex + 1);
179
+ }
180
+ else if ((input === "n" || key.rightArrow) &&
181
+ !loading &&
182
+ !navigating &&
183
+ hasMore) {
184
+ nextPage();
185
+ setSelectedIndex(0);
186
+ }
187
+ else if ((input === "p" || key.leftArrow) &&
188
+ !loading &&
189
+ !navigating &&
190
+ hasPrev) {
191
+ prevPage();
192
+ setSelectedIndex(0);
193
+ }
194
+ else if (key.return && selectedRun) {
195
+ navigate("benchmark-run-detail", {
196
+ benchmarkRunId: selectedRun.id,
197
+ });
198
+ }
199
+ else if (input === "a" && selectedRun) {
200
+ setShowPopup(true);
201
+ setSelectedOperation(0);
202
+ }
203
+ else if (input === "/") {
204
+ search.enterSearchMode();
205
+ }
206
+ else if (key.escape) {
207
+ if (search.handleEscape()) {
208
+ return;
209
+ }
210
+ goBack();
211
+ }
212
+ });
213
+ // Loading state
214
+ if (loading && benchmarkRuns.length === 0) {
215
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
216
+ { label: "Home" },
217
+ { label: "Benchmarks" },
218
+ { label: "Benchmark Runs", active: true },
219
+ ] }), _jsx(SpinnerComponent, { message: "Loading benchmark runs..." })] }));
220
+ }
221
+ // Error state
222
+ if (error) {
223
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
224
+ { label: "Home" },
225
+ { label: "Benchmarks" },
226
+ { label: "Benchmark Runs", active: true },
227
+ ] }), _jsx(ErrorMessage, { message: "Failed to list benchmark runs", error: error })] }));
228
+ }
229
+ // Main list view
230
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
231
+ { label: "Home" },
232
+ { label: "Benchmarks" },
233
+ { label: "Benchmark Runs", active: true },
234
+ ] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search benchmark runs..." }), !showPopup && (_jsx(Table, { data: benchmarkRuns, keyExtractor: (run) => run.id, selectedIndex: selectedIndex, title: `benchmark_runs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No benchmark runs 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 && selectedRun && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedRun, operations: operations.map((op) => ({
235
+ key: op.key,
236
+ label: op.label,
237
+ color: op.color,
238
+ icon: op.icon,
239
+ shortcut: op.key === "view_details"
240
+ ? "v"
241
+ : op.key === "view_scenarios"
242
+ ? "s"
243
+ : "",
244
+ })), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
245
+ {
246
+ icon: `${figures.arrowLeft}${figures.arrowRight}`,
247
+ label: "Page",
248
+ condition: hasMore || hasPrev,
249
+ },
250
+ { key: "Enter", label: "Details" },
251
+ { key: "a", label: "Actions" },
252
+ { key: "/", label: "Search" },
253
+ { key: "Esc", label: "Back" },
254
+ ] })] }));
255
+ }
@@ -14,12 +14,15 @@ export function MenuScreen() {
14
14
  case "snapshots":
15
15
  navigate("snapshot-list");
16
16
  break;
17
- case "network-policies":
18
- navigate("network-policy-list");
17
+ case "settings":
18
+ navigate("settings-menu");
19
19
  break;
20
20
  case "objects":
21
21
  navigate("object-list");
22
22
  break;
23
+ case "benchmarks":
24
+ navigate("benchmark-menu");
25
+ break;
23
26
  default:
24
27
  // Fallback for any other screen names
25
28
  navigate(key);
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ScenarioRunDetailScreen - Detail page for scenario runs
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Text } from "ink";
8
+ import figures from "figures";
9
+ import { useNavigation } from "../store/navigationStore.js";
10
+ import { useBenchmarkStore, } from "../store/benchmarkStore.js";
11
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
12
+ import { getScenarioRun } from "../services/benchmarkService.js";
13
+ import { SpinnerComponent } from "../components/Spinner.js";
14
+ import { ErrorMessage } from "../components/ErrorMessage.js";
15
+ import { Breadcrumb } from "../components/Breadcrumb.js";
16
+ import { colors } from "../utils/theme.js";
17
+ export function ScenarioRunDetailScreen({ scenarioRunId, benchmarkRunId, }) {
18
+ const { goBack, navigate } = useNavigation();
19
+ const scenarioRuns = useBenchmarkStore((state) => state.scenarioRuns);
20
+ const [loading, setLoading] = React.useState(false);
21
+ const [error, setError] = React.useState(null);
22
+ const [fetchedRun, setFetchedRun] = React.useState(null);
23
+ // Find run in store first
24
+ const runFromStore = scenarioRuns.find((r) => r.id === scenarioRunId);
25
+ // Polling function
26
+ const pollRun = React.useCallback(async () => {
27
+ if (!scenarioRunId)
28
+ return null;
29
+ return getScenarioRun(scenarioRunId);
30
+ }, [scenarioRunId]);
31
+ // Fetch run from API if not in store
32
+ React.useEffect(() => {
33
+ if (scenarioRunId && !loading && !fetchedRun) {
34
+ setLoading(true);
35
+ setError(null);
36
+ getScenarioRun(scenarioRunId)
37
+ .then((run) => {
38
+ setFetchedRun(run);
39
+ setLoading(false);
40
+ })
41
+ .catch((err) => {
42
+ setError(err);
43
+ setLoading(false);
44
+ });
45
+ }
46
+ }, [scenarioRunId, loading, fetchedRun]);
47
+ // Use fetched run for full details, fall back to store for basic display
48
+ const run = fetchedRun || runFromStore;
49
+ // Build breadcrumb items
50
+ const buildBreadcrumbItems = (lastLabel, active = true) => {
51
+ const items = [
52
+ { label: "Home" },
53
+ { label: "Benchmarks" },
54
+ ];
55
+ if (benchmarkRunId) {
56
+ items.push({ label: `Run: ${benchmarkRunId.substring(0, 8)}...` });
57
+ }
58
+ items.push({ label: "Scenario Runs" });
59
+ items.push({ label: lastLabel, active });
60
+ return items;
61
+ };
62
+ // Show loading state
63
+ if (!run && scenarioRunId && !error) {
64
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: buildBreadcrumbItems("Loading...") }), _jsx(SpinnerComponent, { message: "Loading scenario run details..." })] }));
65
+ }
66
+ // Show error state
67
+ if (error && !run) {
68
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: buildBreadcrumbItems("Error") }), _jsx(ErrorMessage, { message: "Failed to load scenario run details", error: error })] }));
69
+ }
70
+ // Show not found error
71
+ if (!run) {
72
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: buildBreadcrumbItems("Not Found") }), _jsx(ErrorMessage, { message: `Scenario run ${scenarioRunId || "unknown"} not found`, error: new Error("Scenario run not found") })] }));
73
+ }
74
+ // Build detail sections
75
+ const detailSections = [];
76
+ // Basic details section
77
+ const basicFields = [];
78
+ if (run.start_time_ms) {
79
+ basicFields.push({
80
+ label: "Started",
81
+ value: formatTimestamp(run.start_time_ms),
82
+ });
83
+ }
84
+ const endTimeMs = run.start_time_ms && run.duration_ms
85
+ ? run.start_time_ms + run.duration_ms
86
+ : undefined;
87
+ if (endTimeMs) {
88
+ basicFields.push({
89
+ label: "Ended",
90
+ value: formatTimestamp(endTimeMs),
91
+ });
92
+ }
93
+ if (run.scenario_id) {
94
+ basicFields.push({
95
+ label: "Scenario ID",
96
+ value: _jsx(Text, { color: colors.idColor, children: run.scenario_id }),
97
+ });
98
+ }
99
+ if (run.benchmark_run_id) {
100
+ basicFields.push({
101
+ label: "Benchmark Run ID",
102
+ value: _jsx(Text, { color: colors.idColor, children: run.benchmark_run_id }),
103
+ });
104
+ }
105
+ if (basicFields.length > 0) {
106
+ detailSections.push({
107
+ title: "Details",
108
+ icon: figures.squareSmallFilled,
109
+ color: colors.warning,
110
+ fields: basicFields,
111
+ });
112
+ }
113
+ // Results section
114
+ const score = run.scoring_contract_result?.score;
115
+ if (score !== undefined) {
116
+ detailSections.push({
117
+ title: "Results",
118
+ icon: figures.tick,
119
+ color: colors.success,
120
+ fields: [
121
+ {
122
+ label: "Score",
123
+ value: _jsx(Text, { color: colors.info, children: score }),
124
+ },
125
+ ],
126
+ });
127
+ }
128
+ // Metadata section
129
+ if (run.metadata && Object.keys(run.metadata).length > 0) {
130
+ const metadataFields = Object.entries(run.metadata).map(([key, value]) => ({
131
+ label: key,
132
+ value: value,
133
+ }));
134
+ detailSections.push({
135
+ title: "Metadata",
136
+ icon: figures.identical,
137
+ color: colors.secondary,
138
+ fields: metadataFields,
139
+ });
140
+ }
141
+ // Operations available for scenario runs
142
+ const operations = [];
143
+ if (run.benchmark_run_id) {
144
+ operations.push({
145
+ key: "view-benchmark-run",
146
+ label: "View Benchmark Run",
147
+ color: colors.info,
148
+ icon: figures.arrowRight,
149
+ shortcut: "b",
150
+ });
151
+ }
152
+ // Handle operation selection
153
+ const handleOperation = async (operation, resource) => {
154
+ switch (operation) {
155
+ case "view-benchmark-run":
156
+ if (resource.benchmark_run_id) {
157
+ navigate("benchmark-run-detail", {
158
+ benchmarkRunId: resource.benchmark_run_id,
159
+ });
160
+ }
161
+ break;
162
+ }
163
+ };
164
+ // Build detailed info lines for full details view
165
+ const buildDetailLines = (r) => {
166
+ const lines = [];
167
+ // Core Information
168
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Scenario Run Details" }, "core-title"));
169
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", r.id] }, "core-id"));
170
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", r.name || "(none)"] }, "core-name"));
171
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", r.state] }, "core-status"));
172
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Scenario ID: ", r.scenario_id] }, "core-scenario"));
173
+ if (r.benchmark_run_id) {
174
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Benchmark Run ID: ", r.benchmark_run_id] }, "core-benchmark-run"));
175
+ }
176
+ if (r.start_time_ms) {
177
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "core-created"));
178
+ }
179
+ const detailEndTimeMs = r.start_time_ms && r.duration_ms
180
+ ? r.start_time_ms + r.duration_ms
181
+ : undefined;
182
+ if (detailEndTimeMs) {
183
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "core-ended"));
184
+ }
185
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
186
+ // Results
187
+ const detailScore = r.scoring_contract_result?.score;
188
+ if (detailScore !== undefined) {
189
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Results" }, "results-title"));
190
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Score: ", detailScore] }, "results-score"));
191
+ lines.push(_jsx(Text, { children: " " }, "results-space"));
192
+ }
193
+ // Metadata
194
+ if (r.metadata && Object.keys(r.metadata).length > 0) {
195
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
196
+ Object.entries(r.metadata).forEach(([key, value], idx) => {
197
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
198
+ });
199
+ lines.push(_jsx(Text, { children: " " }, "meta-space"));
200
+ }
201
+ // Raw JSON
202
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
203
+ const jsonLines = JSON.stringify(r, null, 2).split("\n");
204
+ jsonLines.forEach((line, idx) => {
205
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
206
+ });
207
+ return lines;
208
+ };
209
+ // Check if run is still in progress for polling
210
+ const isRunning = run.state === "running" || run.state === "scoring";
211
+ // Build breadcrumb prefix
212
+ const breadcrumbPrefix = [{ label: "Home" }, { label: "Benchmarks" }];
213
+ if (benchmarkRunId) {
214
+ breadcrumbPrefix.push({
215
+ label: `Run: ${benchmarkRunId.substring(0, 8)}...`,
216
+ });
217
+ }
218
+ breadcrumbPrefix.push({ label: "Scenario Runs" });
219
+ return (_jsx(ResourceDetailPage, { resource: run, resourceType: "Scenario Runs", getDisplayName: (r) => r.name || r.id, getId: (r) => r.id, getStatus: (r) => r.state, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: isRunning ? pollRun : undefined, breadcrumbPrefix: breadcrumbPrefix }));
220
+ }