@runloop/rl-cli 1.7.1 → 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,189 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * BenchmarkRunDetailScreen - Detail page for benchmark 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 { getBenchmarkRun } 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 BenchmarkRunDetailScreen({ benchmarkRunId, }) {
18
+ const { goBack, navigate } = useNavigation();
19
+ const benchmarkRuns = useBenchmarkStore((state) => state.benchmarkRuns);
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 = benchmarkRuns.find((r) => r.id === benchmarkRunId);
25
+ // Polling function
26
+ const pollRun = React.useCallback(async () => {
27
+ if (!benchmarkRunId)
28
+ return null;
29
+ return getBenchmarkRun(benchmarkRunId);
30
+ }, [benchmarkRunId]);
31
+ // Fetch run from API if not in store
32
+ React.useEffect(() => {
33
+ if (benchmarkRunId && !loading && !fetchedRun) {
34
+ setLoading(true);
35
+ setError(null);
36
+ getBenchmarkRun(benchmarkRunId)
37
+ .then((run) => {
38
+ setFetchedRun(run);
39
+ setLoading(false);
40
+ })
41
+ .catch((err) => {
42
+ setError(err);
43
+ setLoading(false);
44
+ });
45
+ }
46
+ }, [benchmarkRunId, loading, fetchedRun]);
47
+ // Use fetched run for full details, fall back to store for basic display
48
+ const run = fetchedRun || runFromStore;
49
+ // Show loading state
50
+ if (!run && benchmarkRunId && !error) {
51
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
52
+ { label: "Home" },
53
+ { label: "Benchmarks" },
54
+ { label: "Benchmark Runs" },
55
+ { label: "Loading...", active: true },
56
+ ] }), _jsx(SpinnerComponent, { message: "Loading benchmark run details..." })] }));
57
+ }
58
+ // Show error state
59
+ if (error && !run) {
60
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
61
+ { label: "Home" },
62
+ { label: "Benchmarks" },
63
+ { label: "Benchmark Runs" },
64
+ { label: "Error", active: true },
65
+ ] }), _jsx(ErrorMessage, { message: "Failed to load benchmark run details", error: error })] }));
66
+ }
67
+ // Show not found error
68
+ if (!run) {
69
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
70
+ { label: "Home" },
71
+ { label: "Benchmarks" },
72
+ { label: "Benchmark Runs" },
73
+ { label: "Not Found", active: true },
74
+ ] }), _jsx(ErrorMessage, { message: `Benchmark run ${benchmarkRunId || "unknown"} not found`, error: new Error("Benchmark run not found") })] }));
75
+ }
76
+ // Build detail sections
77
+ const detailSections = [];
78
+ // Basic details section
79
+ const basicFields = [];
80
+ if (run.start_time_ms) {
81
+ basicFields.push({
82
+ label: "Started",
83
+ value: formatTimestamp(run.start_time_ms),
84
+ });
85
+ }
86
+ const endTimeMs = run.start_time_ms && run.duration_ms
87
+ ? run.start_time_ms + run.duration_ms
88
+ : undefined;
89
+ if (endTimeMs) {
90
+ basicFields.push({
91
+ label: "Ended",
92
+ value: formatTimestamp(endTimeMs),
93
+ });
94
+ }
95
+ if (run.benchmark_id) {
96
+ basicFields.push({
97
+ label: "Benchmark ID",
98
+ value: _jsx(Text, { color: colors.idColor, children: run.benchmark_id }),
99
+ });
100
+ }
101
+ if (run.score !== undefined && run.score !== null) {
102
+ basicFields.push({
103
+ label: "Score",
104
+ value: _jsx(Text, { color: colors.info, children: run.score }),
105
+ });
106
+ }
107
+ if (basicFields.length > 0) {
108
+ detailSections.push({
109
+ title: "Details",
110
+ icon: figures.squareSmallFilled,
111
+ color: colors.warning,
112
+ fields: basicFields,
113
+ });
114
+ }
115
+ // Metadata section
116
+ if (run.metadata && Object.keys(run.metadata).length > 0) {
117
+ const metadataFields = Object.entries(run.metadata).map(([key, value]) => ({
118
+ label: key,
119
+ value: value,
120
+ }));
121
+ detailSections.push({
122
+ title: "Metadata",
123
+ icon: figures.identical,
124
+ color: colors.secondary,
125
+ fields: metadataFields,
126
+ });
127
+ }
128
+ // Operations available for benchmark runs
129
+ const operations = [
130
+ {
131
+ key: "view-scenarios",
132
+ label: "View Scenario Runs",
133
+ color: colors.info,
134
+ icon: figures.arrowRight,
135
+ shortcut: "s",
136
+ },
137
+ ];
138
+ // Handle operation selection
139
+ const handleOperation = async (operation, resource) => {
140
+ switch (operation) {
141
+ case "view-scenarios":
142
+ navigate("scenario-run-list", { benchmarkRunId: resource.id });
143
+ break;
144
+ }
145
+ };
146
+ // Build detailed info lines for full details view
147
+ const buildDetailLines = (r) => {
148
+ const lines = [];
149
+ // Core Information
150
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Benchmark Run Details" }, "core-title"));
151
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", r.id] }, "core-id"));
152
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", r.name || "(none)"] }, "core-name"));
153
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", r.state] }, "core-status"));
154
+ if (r.benchmark_id) {
155
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Benchmark ID: ", r.benchmark_id] }, "core-benchmark"));
156
+ }
157
+ if (r.start_time_ms) {
158
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "core-created"));
159
+ }
160
+ const detailEndTimeMs = r.start_time_ms && r.duration_ms
161
+ ? r.start_time_ms + r.duration_ms
162
+ : undefined;
163
+ if (detailEndTimeMs) {
164
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "core-ended"));
165
+ }
166
+ if (r.score !== undefined && r.score !== null) {
167
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Score: ", r.score] }, "core-score"));
168
+ }
169
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
170
+ // Metadata
171
+ if (r.metadata && Object.keys(r.metadata).length > 0) {
172
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
173
+ Object.entries(r.metadata).forEach(([key, value], idx) => {
174
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
175
+ });
176
+ lines.push(_jsx(Text, { children: " " }, "meta-space"));
177
+ }
178
+ // Raw JSON
179
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
180
+ const jsonLines = JSON.stringify(r, null, 2).split("\n");
181
+ jsonLines.forEach((line, idx) => {
182
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
183
+ });
184
+ return lines;
185
+ };
186
+ // Check if run is still in progress for polling
187
+ const isRunning = run.state === "running";
188
+ return (_jsx(ResourceDetailPage, { resource: run, resourceType: "Benchmark 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: [{ label: "Home" }, { label: "Benchmarks" }] }));
189
+ }
@@ -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);