@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,245 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ScenarioRunListScreen - List view for scenario 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 { listScenarioRuns } from "../services/benchmarkService.js";
24
+ export function ScenarioRunListScreen({ benchmarkRunId, }) {
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 timeWidth = 18;
48
+ const baseWidth = fixedWidth + idWidth + statusWidth + scoreWidth + timeWidth;
49
+ const remainingWidth = terminalWidth - baseWidth;
50
+ const nameWidth = Math.min(60, Math.max(15, remainingWidth));
51
+ // Fetch function for pagination hook
52
+ const fetchPage = React.useCallback(async (params) => {
53
+ const result = await listScenarioRuns({
54
+ limit: params.limit,
55
+ startingAfter: params.startingAt,
56
+ benchmarkRunId,
57
+ });
58
+ return {
59
+ items: result.scenarioRuns,
60
+ hasMore: result.hasMore,
61
+ totalCount: result.totalCount,
62
+ };
63
+ }, [benchmarkRunId]);
64
+ // Use the shared pagination hook
65
+ const { items: scenarioRuns, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
66
+ fetchPage,
67
+ pageSize: PAGE_SIZE,
68
+ getItemId: (run) => run.id,
69
+ pollInterval: 5000,
70
+ pollingEnabled: !showPopup && !search.searchMode,
71
+ deps: [PAGE_SIZE, benchmarkRunId],
72
+ });
73
+ // Operations for scenario runs
74
+ const operations = React.useMemo(() => [
75
+ {
76
+ key: "view_details",
77
+ label: "View Details",
78
+ color: colors.primary,
79
+ icon: figures.pointer,
80
+ },
81
+ ], []);
82
+ // Build columns
83
+ const columns = React.useMemo(() => [
84
+ createTextColumn("id", "ID", (run) => run.id, {
85
+ width: idWidth + 1,
86
+ color: colors.idColor,
87
+ dimColor: false,
88
+ bold: false,
89
+ }),
90
+ createTextColumn("name", "Name", (run) => run.name || "", {
91
+ width: nameWidth,
92
+ }),
93
+ createComponentColumn("status", "Status", (run, _index, isSelected) => {
94
+ const statusDisplay = getStatusDisplay(run.state);
95
+ const text = statusDisplay.text
96
+ .slice(0, statusWidth)
97
+ .padEnd(statusWidth, " ");
98
+ return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
99
+ }, { width: statusWidth }),
100
+ createTextColumn("score", "Score", (run) => {
101
+ const score = run.scoring_contract_result?.score;
102
+ return score !== undefined ? String(score) : "";
103
+ }, {
104
+ width: scoreWidth,
105
+ color: colors.info,
106
+ }),
107
+ createTextColumn("created", "Created", (run) => run.start_time_ms ? formatTimeAgo(run.start_time_ms) : "", {
108
+ width: timeWidth,
109
+ color: colors.textDim,
110
+ dimColor: false,
111
+ bold: false,
112
+ }),
113
+ ], [idWidth, nameWidth, statusWidth, scoreWidth, timeWidth]);
114
+ // Handle Ctrl+C to exit
115
+ useExitOnCtrlC();
116
+ // Ensure selected index is within bounds
117
+ React.useEffect(() => {
118
+ if (scenarioRuns.length > 0 && selectedIndex >= scenarioRuns.length) {
119
+ setSelectedIndex(Math.max(0, scenarioRuns.length - 1));
120
+ }
121
+ }, [scenarioRuns.length, selectedIndex]);
122
+ const selectedRun = scenarioRuns[selectedIndex];
123
+ // Calculate pagination info for display
124
+ const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
125
+ const startIndex = currentPage * PAGE_SIZE;
126
+ const endIndex = startIndex + scenarioRuns.length;
127
+ useInput((input, key) => {
128
+ // Handle search mode input
129
+ if (search.searchMode) {
130
+ if (key.escape) {
131
+ search.cancelSearch();
132
+ }
133
+ return;
134
+ }
135
+ // Handle popup navigation
136
+ if (showPopup) {
137
+ if (key.upArrow && selectedOperation > 0) {
138
+ setSelectedOperation(selectedOperation - 1);
139
+ }
140
+ else if (key.downArrow && selectedOperation < operations.length - 1) {
141
+ setSelectedOperation(selectedOperation + 1);
142
+ }
143
+ else if (key.return) {
144
+ setShowPopup(false);
145
+ const operationKey = operations[selectedOperation].key;
146
+ if (operationKey === "view_details") {
147
+ navigate("scenario-run-detail", {
148
+ scenarioRunId: selectedRun.id,
149
+ benchmarkRunId,
150
+ });
151
+ }
152
+ }
153
+ else if (input === "v" && selectedRun) {
154
+ setShowPopup(false);
155
+ navigate("scenario-run-detail", {
156
+ scenarioRunId: selectedRun.id,
157
+ benchmarkRunId,
158
+ });
159
+ }
160
+ else if (key.escape || input === "q") {
161
+ setShowPopup(false);
162
+ setSelectedOperation(0);
163
+ }
164
+ return;
165
+ }
166
+ const pageRuns = scenarioRuns.length;
167
+ // Handle list view navigation
168
+ if (key.upArrow && selectedIndex > 0) {
169
+ setSelectedIndex(selectedIndex - 1);
170
+ }
171
+ else if (key.downArrow && selectedIndex < pageRuns - 1) {
172
+ setSelectedIndex(selectedIndex + 1);
173
+ }
174
+ else if ((input === "n" || key.rightArrow) &&
175
+ !loading &&
176
+ !navigating &&
177
+ hasMore) {
178
+ nextPage();
179
+ setSelectedIndex(0);
180
+ }
181
+ else if ((input === "p" || key.leftArrow) &&
182
+ !loading &&
183
+ !navigating &&
184
+ hasPrev) {
185
+ prevPage();
186
+ setSelectedIndex(0);
187
+ }
188
+ else if (key.return && selectedRun) {
189
+ navigate("scenario-run-detail", {
190
+ scenarioRunId: selectedRun.id,
191
+ benchmarkRunId,
192
+ });
193
+ }
194
+ else if (input === "a" && selectedRun) {
195
+ setShowPopup(true);
196
+ setSelectedOperation(0);
197
+ }
198
+ else if (input === "/") {
199
+ search.enterSearchMode();
200
+ }
201
+ else if (key.escape) {
202
+ if (search.handleEscape()) {
203
+ return;
204
+ }
205
+ goBack();
206
+ }
207
+ });
208
+ // Build breadcrumb items
209
+ const breadcrumbItems = [
210
+ { label: "Home" },
211
+ { label: "Benchmarks" },
212
+ ];
213
+ if (benchmarkRunId) {
214
+ breadcrumbItems.push({
215
+ label: `Run: ${benchmarkRunId.substring(0, 8)}...`,
216
+ });
217
+ }
218
+ breadcrumbItems.push({ label: "Scenario Runs", active: true });
219
+ // Loading state
220
+ if (loading && scenarioRuns.length === 0) {
221
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading scenario runs..." })] }));
222
+ }
223
+ // Error state
224
+ if (error) {
225
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(ErrorMessage, { message: "Failed to list scenario runs", error: error })] }));
226
+ }
227
+ // Main list view
228
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search scenario runs..." }), !showPopup && (_jsx(Table, { data: scenarioRuns, keyExtractor: (run) => run.id, selectedIndex: selectedIndex, title: `scenario_runs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No scenario 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] }), benchmarkRunId && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.info, children: ["Run: ", benchmarkRunId.substring(0, 8), "..."] })] }))] })), showPopup && selectedRun && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedRun, operations: operations.map((op) => ({
229
+ key: op.key,
230
+ label: op.label,
231
+ color: op.color,
232
+ icon: op.icon,
233
+ shortcut: op.key === "view_details" ? "v" : "",
234
+ })), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
235
+ {
236
+ icon: `${figures.arrowLeft}${figures.arrowRight}`,
237
+ label: "Page",
238
+ condition: hasMore || hasPrev,
239
+ },
240
+ { key: "Enter", label: "Details" },
241
+ { key: "a", label: "Actions" },
242
+ { key: "/", label: "Search" },
243
+ { key: "Esc", label: "Back" },
244
+ ] })] }));
245
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { SecretCreatePage } from "../components/SecretCreatePage.js";
4
+ export function SecretCreateScreen() {
5
+ const { goBack, navigate } = useNavigation();
6
+ return (_jsx(SecretCreatePage, { onBack: goBack, onCreate: (secret) => navigate("secret-detail", { secretId: secret.id }) }));
7
+ }
@@ -0,0 +1,198 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SecretDetailScreen - Detail page for secrets
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 { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
11
+ import { getClient } from "../utils/client.js";
12
+ import { SpinnerComponent } from "../components/Spinner.js";
13
+ import { ErrorMessage } from "../components/ErrorMessage.js";
14
+ import { Breadcrumb } from "../components/Breadcrumb.js";
15
+ import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
16
+ import { colors } from "../utils/theme.js";
17
+ export function SecretDetailScreen({ secretId }) {
18
+ const { goBack } = useNavigation();
19
+ const [loading, setLoading] = React.useState(false);
20
+ const [error, setError] = React.useState(null);
21
+ const [secret, setSecret] = React.useState(null);
22
+ const [deleting, setDeleting] = React.useState(false);
23
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
24
+ // Fetch secret from API
25
+ React.useEffect(() => {
26
+ if (secretId && !loading && !secret) {
27
+ setLoading(true);
28
+ setError(null);
29
+ const client = getClient();
30
+ // Secrets API doesn't have a direct get by ID, so we list all and find
31
+ client.secrets
32
+ .list({ limit: 5000 })
33
+ .then((result) => {
34
+ const found = result.secrets?.find((s) => s.id === secretId || s.name === secretId);
35
+ if (found) {
36
+ setSecret(found);
37
+ }
38
+ else {
39
+ setError(new Error("Secret not found"));
40
+ }
41
+ setLoading(false);
42
+ })
43
+ .catch((err) => {
44
+ setError(err);
45
+ setLoading(false);
46
+ });
47
+ }
48
+ }, [secretId, loading, secret]);
49
+ // Show loading state while fetching or before fetch starts
50
+ if (!secret && secretId && !error) {
51
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
52
+ { label: "Settings" },
53
+ { label: "Secrets" },
54
+ { label: "Loading...", active: true },
55
+ ] }), _jsx(SpinnerComponent, { message: "Loading secret details..." })] }));
56
+ }
57
+ // Show error state if fetch failed
58
+ if (error && !secret) {
59
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
60
+ { label: "Settings" },
61
+ { label: "Secrets" },
62
+ { label: "Error", active: true },
63
+ ] }), _jsx(ErrorMessage, { message: "Failed to load secret details", error: error })] }));
64
+ }
65
+ // Show error if no secret found
66
+ if (!secret) {
67
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
68
+ { label: "Settings" },
69
+ { label: "Secrets" },
70
+ { label: "Not Found", active: true },
71
+ ] }), _jsx(ErrorMessage, { message: `Secret ${secretId || "unknown"} not found`, error: new Error("Secret not found") })] }));
72
+ }
73
+ // Build detail sections
74
+ const detailSections = [];
75
+ // Basic details section
76
+ const basicFields = [];
77
+ basicFields.push({
78
+ label: "Name",
79
+ value: secret.name,
80
+ });
81
+ if (secret.create_time_ms) {
82
+ basicFields.push({
83
+ label: "Created",
84
+ value: formatTimestamp(secret.create_time_ms),
85
+ });
86
+ }
87
+ if (secret.update_time_ms) {
88
+ basicFields.push({
89
+ label: "Last Updated",
90
+ value: formatTimestamp(secret.update_time_ms),
91
+ });
92
+ }
93
+ detailSections.push({
94
+ title: "Details",
95
+ icon: figures.squareSmallFilled,
96
+ color: colors.warning,
97
+ fields: basicFields,
98
+ });
99
+ // Security notice section
100
+ detailSections.push({
101
+ title: "Security",
102
+ icon: figures.warning,
103
+ color: colors.info,
104
+ fields: [
105
+ {
106
+ label: "Value",
107
+ value: (_jsx(Text, { color: colors.textDim, dimColor: true, children: "Secret values are never displayed for security reasons" })),
108
+ },
109
+ ],
110
+ });
111
+ // Operations available for secrets (only delete - secrets are immutable)
112
+ const operations = [
113
+ {
114
+ key: "delete",
115
+ label: "Delete Secret",
116
+ color: colors.error,
117
+ icon: figures.cross,
118
+ shortcut: "d",
119
+ },
120
+ ];
121
+ // Handle operation selection
122
+ const handleOperation = async (operation, _resource) => {
123
+ switch (operation) {
124
+ case "delete":
125
+ // Show confirmation dialog
126
+ setShowDeleteConfirm(true);
127
+ break;
128
+ }
129
+ };
130
+ // Execute delete after confirmation
131
+ const executeDelete = async () => {
132
+ if (!secret)
133
+ return;
134
+ setShowDeleteConfirm(false);
135
+ setDeleting(true);
136
+ try {
137
+ const client = getClient();
138
+ await client.secrets.delete(secret.name);
139
+ goBack();
140
+ }
141
+ catch (err) {
142
+ setError(err);
143
+ setDeleting(false);
144
+ }
145
+ };
146
+ // Build detailed info lines for full details view
147
+ const buildDetailLines = (s) => {
148
+ const lines = [];
149
+ // Core Information
150
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Secret Details" }, "core-title"));
151
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", s.id] }, "core-id"));
152
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", s.name] }, "core-name"));
153
+ if (s.create_time_ms) {
154
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(s.create_time_ms).toLocaleString()] }, "core-created"));
155
+ }
156
+ if (s.update_time_ms) {
157
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Last Updated: ", new Date(s.update_time_ms).toLocaleString()] }, "core-updated"));
158
+ }
159
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
160
+ // Security Notice
161
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Security Notice" }, "security-title"));
162
+ lines.push(_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "Secret values are write-only and cannot be retrieved."] }, "security-notice"));
163
+ lines.push(_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "To change a secret, delete it and create a new one with the same name."] }, "security-notice2"));
164
+ lines.push(_jsx(Text, { children: " " }, "security-space"));
165
+ // Raw JSON (without value)
166
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
167
+ const jsonObj = {
168
+ id: s.id,
169
+ name: s.name,
170
+ create_time_ms: s.create_time_ms,
171
+ update_time_ms: s.update_time_ms,
172
+ };
173
+ const jsonLines = JSON.stringify(jsonObj, null, 2).split("\n");
174
+ jsonLines.forEach((line, idx) => {
175
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
176
+ });
177
+ return lines;
178
+ };
179
+ // Show delete confirmation
180
+ if (showDeleteConfirm && secret) {
181
+ return (_jsx(ConfirmationPrompt, { title: "Delete Secret", message: `Are you sure you want to delete "${secret.name}"?`, details: "This action cannot be undone. Any devboxes using this secret will no longer have access to it.", breadcrumbItems: [
182
+ { label: "Settings" },
183
+ { label: "Secrets" },
184
+ { label: secret.name || secret.id },
185
+ { label: "Delete", active: true },
186
+ ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
187
+ }
188
+ // Show deleting state
189
+ if (deleting) {
190
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
191
+ { label: "Settings" },
192
+ { label: "Secrets" },
193
+ { label: secret.name || secret.id },
194
+ { label: "Deleting...", active: true },
195
+ ] }), _jsx(SpinnerComponent, { message: "Deleting secret..." })] }));
196
+ }
197
+ return (_jsx(ResourceDetailPage, { resource: secret, resourceType: "Secrets", breadcrumbPrefix: [{ label: "Settings" }], getDisplayName: (s) => s.name || s.id, getId: (s) => s.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
198
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { ListSecretsUI } from "../commands/secret/list.js";
4
+ export function SecretListScreen() {
5
+ const { goBack } = useNavigation();
6
+ return _jsx(ListSecretsUI, { onBack: goBack });
7
+ }
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { SettingsMenu } from "../components/SettingsMenu.js";
4
+ export function SettingsMenuScreen() {
5
+ const { navigate, goBack } = useNavigation();
6
+ const handleSelect = (key) => {
7
+ switch (key) {
8
+ case "network-policies":
9
+ navigate("network-policy-list");
10
+ break;
11
+ case "secrets":
12
+ navigate("secret-list");
13
+ break;
14
+ default:
15
+ // Fallback for any other screen names
16
+ navigate(key);
17
+ }
18
+ };
19
+ const handleBack = () => {
20
+ goBack();
21
+ };
22
+ return _jsx(SettingsMenu, { onSelect: handleSelect, onBack: handleBack });
23
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Benchmark Service - Handles all benchmark-related API calls
3
+ */
4
+ import { getClient } from "../utils/client.js";
5
+ /**
6
+ * List benchmark runs with pagination
7
+ */
8
+ export async function listBenchmarkRuns(options) {
9
+ const client = getClient();
10
+ const queryParams = {
11
+ limit: options.limit,
12
+ };
13
+ if (options.startingAfter) {
14
+ queryParams.starting_after = options.startingAfter;
15
+ }
16
+ const page = await client.benchmarkRuns.list(queryParams);
17
+ const benchmarkRuns = page.runs || [];
18
+ return {
19
+ benchmarkRuns,
20
+ totalCount: page.total_count || benchmarkRuns.length,
21
+ hasMore: page.has_more || false,
22
+ };
23
+ }
24
+ /**
25
+ * Get benchmark run by ID
26
+ */
27
+ export async function getBenchmarkRun(id) {
28
+ const client = getClient();
29
+ return client.benchmarkRuns.retrieve(id);
30
+ }
31
+ /**
32
+ * List scenario runs with pagination
33
+ */
34
+ export async function listScenarioRuns(options) {
35
+ const client = getClient();
36
+ // If we have a benchmark run ID, use the dedicated endpoint
37
+ if (options.benchmarkRunId) {
38
+ const queryParams = {
39
+ limit: options.limit,
40
+ };
41
+ if (options.startingAfter) {
42
+ queryParams.starting_after = options.startingAfter;
43
+ }
44
+ const page = await client.benchmarkRuns.listScenarioRuns(options.benchmarkRunId, queryParams);
45
+ const scenarioRuns = page.runs || [];
46
+ return {
47
+ scenarioRuns,
48
+ totalCount: page.total_count || scenarioRuns.length,
49
+ hasMore: page.has_more || false,
50
+ };
51
+ }
52
+ // Otherwise, list all scenario runs
53
+ const queryParams = {
54
+ limit: options.limit,
55
+ };
56
+ if (options.startingAfter) {
57
+ queryParams.starting_after = options.startingAfter;
58
+ }
59
+ const page = await client.scenarios.runs.list(queryParams);
60
+ const scenarioRuns = page.runs || [];
61
+ return {
62
+ scenarioRuns,
63
+ totalCount: page.total_count || scenarioRuns.length,
64
+ hasMore: page.has_more || false,
65
+ };
66
+ }
67
+ /**
68
+ * Get scenario run by ID
69
+ */
70
+ export async function getScenarioRun(id) {
71
+ const client = getClient();
72
+ return client.scenarios.runs.retrieve(id);
73
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Benchmark Store - Manages benchmark run and scenario run state
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useBenchmarkStore = create((set, get) => ({
7
+ // Initial benchmark run state
8
+ benchmarkRuns: [],
9
+ benchmarkRunsLoading: false,
10
+ benchmarkRunsError: null,
11
+ benchmarkRunsTotalCount: 0,
12
+ benchmarkRunsHasMore: false,
13
+ benchmarkRunsCurrentPage: 0,
14
+ // Initial scenario run state
15
+ scenarioRuns: [],
16
+ scenarioRunsLoading: false,
17
+ scenarioRunsError: null,
18
+ scenarioRunsTotalCount: 0,
19
+ scenarioRunsHasMore: false,
20
+ scenarioRunsCurrentPage: 0,
21
+ // Filters
22
+ benchmarkRunIdFilter: undefined,
23
+ // Selection
24
+ selectedBenchmarkRunIndex: 0,
25
+ selectedScenarioRunIndex: 0,
26
+ // Caches
27
+ benchmarkRunPageCache: new Map(),
28
+ scenarioRunPageCache: new Map(),
29
+ // Benchmark Run Actions
30
+ setBenchmarkRuns: (runs) => set({ benchmarkRuns: runs }),
31
+ setBenchmarkRunsLoading: (loading) => set({ benchmarkRunsLoading: loading }),
32
+ setBenchmarkRunsError: (error) => set({ benchmarkRunsError: error }),
33
+ setBenchmarkRunsTotalCount: (count) => set({ benchmarkRunsTotalCount: count }),
34
+ setBenchmarkRunsHasMore: (hasMore) => set({ benchmarkRunsHasMore: hasMore }),
35
+ setBenchmarkRunsCurrentPage: (page) => set({ benchmarkRunsCurrentPage: page }),
36
+ setSelectedBenchmarkRunIndex: (index) => set({ selectedBenchmarkRunIndex: index }),
37
+ // Scenario Run Actions
38
+ setScenarioRuns: (runs) => set({ scenarioRuns: runs }),
39
+ setScenarioRunsLoading: (loading) => set({ scenarioRunsLoading: loading }),
40
+ setScenarioRunsError: (error) => set({ scenarioRunsError: error }),
41
+ setScenarioRunsTotalCount: (count) => set({ scenarioRunsTotalCount: count }),
42
+ setScenarioRunsHasMore: (hasMore) => set({ scenarioRunsHasMore: hasMore }),
43
+ setScenarioRunsCurrentPage: (page) => set({ scenarioRunsCurrentPage: page }),
44
+ setSelectedScenarioRunIndex: (index) => set({ selectedScenarioRunIndex: index }),
45
+ setBenchmarkRunIdFilter: (id) => set({ benchmarkRunIdFilter: id }),
46
+ // Cache management
47
+ cacheBenchmarkRunPage: (page, data) => {
48
+ const state = get();
49
+ const cache = state.benchmarkRunPageCache;
50
+ if (cache.size >= MAX_CACHE_SIZE) {
51
+ const oldestKey = cache.keys().next().value;
52
+ if (oldestKey !== undefined) {
53
+ cache.delete(oldestKey);
54
+ }
55
+ }
56
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
57
+ cache.set(page, plainData);
58
+ set({});
59
+ },
60
+ getCachedBenchmarkRunPage: (page) => {
61
+ return get().benchmarkRunPageCache.get(page);
62
+ },
63
+ cacheScenarioRunPage: (page, data) => {
64
+ const state = get();
65
+ const cache = state.scenarioRunPageCache;
66
+ if (cache.size >= MAX_CACHE_SIZE) {
67
+ const oldestKey = cache.keys().next().value;
68
+ if (oldestKey !== undefined) {
69
+ cache.delete(oldestKey);
70
+ }
71
+ }
72
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
73
+ cache.set(page, plainData);
74
+ set({});
75
+ },
76
+ getCachedScenarioRunPage: (page) => {
77
+ return get().scenarioRunPageCache.get(page);
78
+ },
79
+ clearCache: () => {
80
+ const state = get();
81
+ state.benchmarkRunPageCache.clear();
82
+ state.scenarioRunPageCache.clear();
83
+ set({
84
+ benchmarkRunPageCache: new Map(),
85
+ scenarioRunPageCache: new Map(),
86
+ });
87
+ },
88
+ clearAll: () => {
89
+ const state = get();
90
+ state.benchmarkRunPageCache.clear();
91
+ state.scenarioRunPageCache.clear();
92
+ set({
93
+ benchmarkRuns: [],
94
+ benchmarkRunsLoading: false,
95
+ benchmarkRunsError: null,
96
+ benchmarkRunsTotalCount: 0,
97
+ benchmarkRunsHasMore: false,
98
+ benchmarkRunsCurrentPage: 0,
99
+ scenarioRuns: [],
100
+ scenarioRunsLoading: false,
101
+ scenarioRunsError: null,
102
+ scenarioRunsTotalCount: 0,
103
+ scenarioRunsHasMore: false,
104
+ scenarioRunsCurrentPage: 0,
105
+ benchmarkRunIdFilter: undefined,
106
+ selectedBenchmarkRunIndex: 0,
107
+ selectedScenarioRunIndex: 0,
108
+ benchmarkRunPageCache: new Map(),
109
+ scenarioRunPageCache: new Map(),
110
+ });
111
+ },
112
+ getSelectedBenchmarkRun: () => {
113
+ const state = get();
114
+ return state.benchmarkRuns[state.selectedBenchmarkRunIndex];
115
+ },
116
+ getSelectedScenarioRun: () => {
117
+ const state = get();
118
+ return state.scenarioRuns[state.selectedScenarioRunIndex];
119
+ },
120
+ }));