@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.
@@ -5,7 +5,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  */
6
6
  import React from "react";
7
7
  import { Box, Text, useInput } from "ink";
8
- import Spinner from "ink-spinner";
9
8
  import figures from "figures";
10
9
  import { Breadcrumb } from "./Breadcrumb.js";
11
10
  import { NavigationTips } from "./NavigationTips.js";
@@ -14,6 +13,21 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js";
14
13
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
15
14
  import { parseAnyLogEntry } from "../utils/logFormatter.js";
16
15
  import { getDevboxLogs } from "../services/devboxService.js";
16
+ // Color maps - defined outside component to avoid recreation on every render
17
+ const levelColorMap = {
18
+ red: colors.error,
19
+ yellow: colors.warning,
20
+ blue: colors.primary,
21
+ gray: colors.textDim,
22
+ };
23
+ const sourceColorMap = {
24
+ magenta: "#d33682",
25
+ cyan: colors.info,
26
+ green: colors.success,
27
+ yellow: colors.warning,
28
+ gray: colors.textDim,
29
+ white: colors.text,
30
+ };
17
31
  export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Logs", active: true }], onBack, }) => {
18
32
  const [logs, setLogs] = React.useState([]);
19
33
  const [loading, setLoading] = React.useState(true);
@@ -25,15 +39,40 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
25
39
  const [isPolling, setIsPolling] = React.useState(true);
26
40
  // Refs for cleanup
27
41
  const pollIntervalRef = React.useRef(null);
28
- // Calculate viewport
29
- const logsViewport = useViewportHeight({ overhead: 10, minHeight: 10 });
42
+ // Calculate viewport - overhead increased to reduce overdraw/flashing
43
+ const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
30
44
  // Handle Ctrl+C
31
45
  useExitOnCtrlC();
32
- // Fetch logs function
46
+ // Fetch logs function - only update state if logs actually changed
33
47
  const fetchLogs = React.useCallback(async () => {
34
48
  try {
35
49
  const newLogs = await getDevboxLogs(devboxId);
36
- setLogs(newLogs);
50
+ // Only update logs state if the logs have actually changed
51
+ // This prevents unnecessary re-renders that cause flashing in non-tmux terminals
52
+ setLogs((prevLogs) => {
53
+ // Quick length check first
54
+ if (prevLogs.length !== newLogs.length) {
55
+ return newLogs;
56
+ }
57
+ // If same length, check if last log entry is different (most common case for streaming)
58
+ if (newLogs.length > 0) {
59
+ const prevLast = prevLogs[prevLogs.length - 1];
60
+ const newLast = newLogs[newLogs.length - 1];
61
+ // Compare by timestamp and message for efficiency
62
+ if (prevLast &&
63
+ newLast &&
64
+ "timestamp" in prevLast &&
65
+ "timestamp" in newLast &&
66
+ "message" in prevLast &&
67
+ "message" in newLast &&
68
+ prevLast.timestamp === newLast.timestamp &&
69
+ prevLast.message === newLast.message) {
70
+ // Logs haven't changed, return previous state to avoid re-render
71
+ return prevLogs;
72
+ }
73
+ }
74
+ return newLogs;
75
+ });
37
76
  setError(null);
38
77
  if (loading)
39
78
  setLoading(false);
@@ -216,57 +255,79 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
216
255
  }
217
256
  const hasMore = actualScroll + visibleLogs.length < logs.length;
218
257
  const hasLess = actualScroll > 0;
219
- // Color maps
220
- const levelColorMap = {
221
- red: colors.error,
222
- yellow: colors.warning,
223
- blue: colors.primary,
224
- gray: colors.textDim,
225
- };
226
- const sourceColorMap = {
227
- magenta: "#d33682",
228
- cyan: colors.info,
229
- green: colors.success,
230
- yellow: colors.warning,
231
- gray: colors.textDim,
232
- white: colors.text,
233
- };
234
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, height: viewportHeight + 2, children: loading ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.info, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: colors.textDim, children: " Loading logs..." })] })) : error ? (_jsxs(Text, { color: colors.error, children: [figures.cross, " Error: ", error] })) : logs.length === 0 ? (_jsxs(Box, { children: [isPolling && (_jsxs(Text, { color: colors.info, children: [_jsx(Spinner, { type: "dots" }), " "] })), _jsx(Text, { color: colors.textDim, children: "Waiting for logs..." })] })) : (visibleLogs.map((log, index) => {
235
- const parts = parseAnyLogEntry(log);
236
- const sanitizedMessage = sanitizeMessage(parts.message);
237
- const MAX_MESSAGE_LENGTH = 1000;
238
- const fullMessage = sanitizedMessage.length > MAX_MESSAGE_LENGTH
239
- ? sanitizedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
240
- : sanitizedMessage;
241
- const cmd = parts.cmd
242
- ? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
243
- : "";
244
- const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
245
- const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
246
- const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
247
- if (logsWrapMode) {
248
- return (_jsx(Box, { width: contentWidth, flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, index));
258
+ // Build lines array - always render exactly viewportHeight lines for stable structure
259
+ // This prevents Ink from doing structural redraws that cause flashing
260
+ const renderLines = React.useMemo(() => {
261
+ const lines = [];
262
+ if (loading) {
263
+ // First line shows loading message
264
+ lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { color: colors.textDim, children: "\u25CF Loading logs..." }) }, "loading"));
265
+ }
266
+ else if (error) {
267
+ // First line shows error
268
+ lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Error: ", error] }) }, "error"));
269
+ }
270
+ else if (logs.length === 0) {
271
+ // First line shows waiting message
272
+ lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.textDim, children: [isPolling ? "● " : "", "Waiting for logs..."] }) }, "waiting"));
273
+ }
274
+ else {
275
+ // Render visible log entries
276
+ for (let i = 0; i < visibleLogs.length; i++) {
277
+ const log = visibleLogs[i];
278
+ const parts = parseAnyLogEntry(log);
279
+ const sanitizedMessage = sanitizeMessage(parts.message);
280
+ const MAX_MESSAGE_LENGTH = 1000;
281
+ const fullMessage = sanitizedMessage.length > MAX_MESSAGE_LENGTH
282
+ ? sanitizedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
283
+ : sanitizedMessage;
284
+ const cmd = parts.cmd
285
+ ? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
286
+ : "";
287
+ const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
288
+ const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
289
+ const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
290
+ if (logsWrapMode) {
291
+ lines.push(_jsx(Box, { width: contentWidth, flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
292
+ }
293
+ else {
294
+ const shellPart = parts.shellName ? `(${parts.shellName}) ` : "";
295
+ const exitPart = exitCode ? ` ${exitCode}` : "";
296
+ const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
297
+ const suffix = exitPart;
298
+ const availableForMessage = contentWidth - prefix.length - suffix.length;
299
+ let displayMessage;
300
+ if (availableForMessage <= 3) {
301
+ displayMessage = "";
302
+ }
303
+ else if (fullMessage.length <= availableForMessage) {
304
+ displayMessage = fullMessage;
249
305
  }
250
306
  else {
251
- const shellPart = parts.shellName ? `(${parts.shellName}) ` : "";
252
- const exitPart = exitCode ? ` ${exitCode}` : "";
253
- const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
254
- const suffix = exitPart;
255
- const availableForMessage = contentWidth - prefix.length - suffix.length;
256
- let displayMessage;
257
- if (availableForMessage <= 3) {
258
- displayMessage = "";
259
- }
260
- else if (fullMessage.length <= availableForMessage) {
261
- displayMessage = fullMessage;
262
- }
263
- else {
264
- displayMessage =
265
- fullMessage.substring(0, availableForMessage - 3) + "...";
266
- }
267
- return (_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, index));
307
+ displayMessage =
308
+ fullMessage.substring(0, availableForMessage - 3) + "...";
268
309
  }
269
- })) }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "logs"] }), logs.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [actualScroll + 1, "-", Math.min(actualScroll + visibleLogs.length, logs.length), " of", " ", logs.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), isPolling ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.success, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: colors.success, children: " Live" })] })) : (_jsx(Text, { color: colors.textDim, children: "Paused" })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(NavigationTips, { showArrows: true, tips: [
310
+ lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
311
+ }
312
+ }
313
+ }
314
+ // Pad with empty lines to maintain consistent structure
315
+ // This prevents Ink from doing structural redraws
316
+ while (lines.length < viewportHeight) {
317
+ lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { children: " " }) }, `pad-${lines.length}`));
318
+ }
319
+ return lines;
320
+ }, [
321
+ loading,
322
+ error,
323
+ logs.length,
324
+ visibleLogs,
325
+ isPolling,
326
+ logsWrapMode,
327
+ contentWidth,
328
+ viewportHeight,
329
+ ]);
330
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, height: viewportHeight + 2, children: renderLines }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "logs"] }), logs.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [actualScroll + 1, "-", Math.min(actualScroll + visibleLogs.length, logs.length), " of", " ", logs.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), isPolling ? (_jsx(Text, { color: colors.success, children: "\u25CF Live" })) : (_jsx(Text, { color: colors.textDim, children: "\u25CB Paused" })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(NavigationTips, { showArrows: true, tips: [
270
331
  { key: "g/G", label: "Top/Bottom" },
271
332
  { key: "p", label: isPolling ? "Pause" : "Resume" },
272
333
  { key: "w", label: "Wrap" },
@@ -10,6 +10,7 @@ import { useBlueprintStore } from "../store/blueprintStore.js";
10
10
  import { useSnapshotStore } from "../store/snapshotStore.js";
11
11
  import { useNetworkPolicyStore } from "../store/networkPolicyStore.js";
12
12
  import { useObjectStore } from "../store/objectStore.js";
13
+ import { useBenchmarkStore } from "../store/benchmarkStore.js";
13
14
  import { ErrorBoundary } from "../components/ErrorBoundary.js";
14
15
  // Import screen components
15
16
  import { MenuScreen } from "../screens/MenuScreen.js";
@@ -26,9 +27,18 @@ import { SnapshotDetailScreen } from "../screens/SnapshotDetailScreen.js";
26
27
  import { NetworkPolicyListScreen } from "../screens/NetworkPolicyListScreen.js";
27
28
  import { NetworkPolicyDetailScreen } from "../screens/NetworkPolicyDetailScreen.js";
28
29
  import { NetworkPolicyCreateScreen } from "../screens/NetworkPolicyCreateScreen.js";
30
+ import { SettingsMenuScreen } from "../screens/SettingsMenuScreen.js";
31
+ import { SecretListScreen } from "../screens/SecretListScreen.js";
32
+ import { SecretDetailScreen } from "../screens/SecretDetailScreen.js";
33
+ import { SecretCreateScreen } from "../screens/SecretCreateScreen.js";
29
34
  import { ObjectListScreen } from "../screens/ObjectListScreen.js";
30
35
  import { ObjectDetailScreen } from "../screens/ObjectDetailScreen.js";
31
36
  import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
37
+ import { BenchmarkMenuScreen } from "../screens/BenchmarkMenuScreen.js";
38
+ import { BenchmarkRunListScreen } from "../screens/BenchmarkRunListScreen.js";
39
+ import { BenchmarkRunDetailScreen } from "../screens/BenchmarkRunDetailScreen.js";
40
+ import { ScenarioRunListScreen } from "../screens/ScenarioRunListScreen.js";
41
+ import { ScenarioRunDetailScreen } from "../screens/ScenarioRunDetailScreen.js";
32
42
  /**
33
43
  * Router component that renders the current screen
34
44
  * Implements memory cleanup on route changes
@@ -82,6 +92,16 @@ export function Router() {
82
92
  useObjectStore.getState().clearAll();
83
93
  }
84
94
  break;
95
+ case "benchmark-menu":
96
+ case "benchmark-run-list":
97
+ case "benchmark-run-detail":
98
+ case "scenario-run-list":
99
+ case "scenario-run-detail":
100
+ if (!currentScreen.startsWith("benchmark") &&
101
+ !currentScreen.startsWith("scenario")) {
102
+ useBenchmarkStore.getState().clearAll();
103
+ }
104
+ break;
85
105
  }
86
106
  }
87
107
  prevScreenRef.current = currentScreen;
@@ -90,5 +110,5 @@ export function Router() {
90
110
  // and mount new component, preventing race conditions during screen transitions.
91
111
  // The key ensures React treats this as a completely new component tree.
92
112
  // Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
93
- return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
113
+ return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "settings-menu" && (_jsx(SettingsMenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "secret-list" && (_jsx(SecretListScreen, { ...params }, currentScreen)), currentScreen === "secret-detail" && (_jsx(SecretDetailScreen, { ...params }, currentScreen)), currentScreen === "secret-create" && (_jsx(SecretCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen)), currentScreen === "benchmark-menu" && (_jsx(BenchmarkMenuScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-list" && (_jsx(BenchmarkRunListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-detail" && (_jsx(BenchmarkRunDetailScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-list" && (_jsx(ScenarioRunListScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-detail" && (_jsx(ScenarioRunDetailScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
94
114
  }
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { BenchmarkMenu } from "../components/BenchmarkMenu.js";
4
+ export function BenchmarkMenuScreen() {
5
+ const { navigate, goBack } = useNavigation();
6
+ const handleSelect = (key) => {
7
+ switch (key) {
8
+ case "benchmark-runs":
9
+ navigate("benchmark-run-list");
10
+ break;
11
+ case "scenario-runs":
12
+ navigate("scenario-run-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(BenchmarkMenu, { onSelect: handleSelect, onBack: handleBack });
23
+ }
@@ -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
+ }