@runloop/rl-cli 1.7.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -0
- package/dist/commands/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +108 -0
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +183 -0
- package/dist/components/SettingsMenu.js +95 -0
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +80 -0
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +99 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +29 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
- package/dist/screens/BenchmarkRunListScreen.js +275 -0
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +226 -0
- package/dist/screens/ScenarioRunListScreen.js +245 -0
- package/dist/screens/SecretCreateScreen.js +7 -0
- package/dist/screens/SecretDetailScreen.js +198 -0
- package/dist/screens/SecretListScreen.js +7 -0
- package/dist/screens/SettingsMenuScreen.js +26 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +120 -0
- package/dist/services/gatewayConfigService.js +114 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +183 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +80 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -43
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
action: {
|
|
104
|
+
type: "navigate",
|
|
105
|
+
screen: "benchmark-run-detail",
|
|
106
|
+
params: { benchmarkRunId: run.benchmark_run_id },
|
|
107
|
+
hint: "View Run",
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (basicFields.length > 0) {
|
|
112
|
+
detailSections.push({
|
|
113
|
+
title: "Details",
|
|
114
|
+
icon: figures.squareSmallFilled,
|
|
115
|
+
color: colors.warning,
|
|
116
|
+
fields: basicFields,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Results section
|
|
120
|
+
const score = run.scoring_contract_result?.score;
|
|
121
|
+
if (score !== undefined) {
|
|
122
|
+
detailSections.push({
|
|
123
|
+
title: "Results",
|
|
124
|
+
icon: figures.tick,
|
|
125
|
+
color: colors.success,
|
|
126
|
+
fields: [
|
|
127
|
+
{
|
|
128
|
+
label: "Score",
|
|
129
|
+
value: _jsx(Text, { color: colors.info, children: score }),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Metadata section
|
|
135
|
+
if (run.metadata && Object.keys(run.metadata).length > 0) {
|
|
136
|
+
const metadataFields = Object.entries(run.metadata).map(([key, value]) => ({
|
|
137
|
+
label: key,
|
|
138
|
+
value: value,
|
|
139
|
+
}));
|
|
140
|
+
detailSections.push({
|
|
141
|
+
title: "Metadata",
|
|
142
|
+
icon: figures.identical,
|
|
143
|
+
color: colors.secondary,
|
|
144
|
+
fields: metadataFields,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Operations available for scenario runs
|
|
148
|
+
const operations = [];
|
|
149
|
+
if (run.benchmark_run_id) {
|
|
150
|
+
operations.push({
|
|
151
|
+
key: "view-benchmark-run",
|
|
152
|
+
label: "View Benchmark Run",
|
|
153
|
+
color: colors.info,
|
|
154
|
+
icon: figures.arrowRight,
|
|
155
|
+
shortcut: "b",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Handle operation selection
|
|
159
|
+
const handleOperation = async (operation, resource) => {
|
|
160
|
+
switch (operation) {
|
|
161
|
+
case "view-benchmark-run":
|
|
162
|
+
if (resource.benchmark_run_id) {
|
|
163
|
+
navigate("benchmark-run-detail", {
|
|
164
|
+
benchmarkRunId: resource.benchmark_run_id,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
// Build detailed info lines for full details view
|
|
171
|
+
const buildDetailLines = (r) => {
|
|
172
|
+
const lines = [];
|
|
173
|
+
// Core Information
|
|
174
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Scenario Run Details" }, "core-title"));
|
|
175
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", r.id] }, "core-id"));
|
|
176
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", r.name || "(none)"] }, "core-name"));
|
|
177
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", r.state] }, "core-status"));
|
|
178
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Scenario ID: ", r.scenario_id] }, "core-scenario"));
|
|
179
|
+
if (r.benchmark_run_id) {
|
|
180
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Benchmark Run ID: ", r.benchmark_run_id] }, "core-benchmark-run"));
|
|
181
|
+
}
|
|
182
|
+
if (r.start_time_ms) {
|
|
183
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "core-created"));
|
|
184
|
+
}
|
|
185
|
+
const detailEndTimeMs = r.start_time_ms && r.duration_ms
|
|
186
|
+
? r.start_time_ms + r.duration_ms
|
|
187
|
+
: undefined;
|
|
188
|
+
if (detailEndTimeMs) {
|
|
189
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "core-ended"));
|
|
190
|
+
}
|
|
191
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
192
|
+
// Results
|
|
193
|
+
const detailScore = r.scoring_contract_result?.score;
|
|
194
|
+
if (detailScore !== undefined) {
|
|
195
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Results" }, "results-title"));
|
|
196
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Score: ", detailScore] }, "results-score"));
|
|
197
|
+
lines.push(_jsx(Text, { children: " " }, "results-space"));
|
|
198
|
+
}
|
|
199
|
+
// Metadata
|
|
200
|
+
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
201
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
|
|
202
|
+
Object.entries(r.metadata).forEach(([key, value], idx) => {
|
|
203
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
|
|
204
|
+
});
|
|
205
|
+
lines.push(_jsx(Text, { children: " " }, "meta-space"));
|
|
206
|
+
}
|
|
207
|
+
// Raw JSON
|
|
208
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
209
|
+
const jsonLines = JSON.stringify(r, null, 2).split("\n");
|
|
210
|
+
jsonLines.forEach((line, idx) => {
|
|
211
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
212
|
+
});
|
|
213
|
+
return lines;
|
|
214
|
+
};
|
|
215
|
+
// Check if run is still in progress for polling
|
|
216
|
+
const isRunning = run.state === "running" || run.state === "scoring";
|
|
217
|
+
// Build breadcrumb prefix
|
|
218
|
+
const breadcrumbPrefix = [{ label: "Home" }, { label: "Benchmarks" }];
|
|
219
|
+
if (benchmarkRunId) {
|
|
220
|
+
breadcrumbPrefix.push({
|
|
221
|
+
label: `Run: ${benchmarkRunId.substring(0, 8)}...`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
breadcrumbPrefix.push({ label: "Scenario Runs" });
|
|
225
|
+
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 }));
|
|
226
|
+
}
|
|
@@ -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
|
+
}
|