Skip to content

Commit bda1e91

Browse files
authored
feat(benchmark): adding benchmark runs and scenarios to the cli (#84)
Adding benchmark beta feature
1 parent 69aa213 commit bda1e91

19 files changed

Lines changed: 2395 additions & 43 deletions

.github/workflows/pr-title.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ jobs:
3232
cli
3333
mcp
3434
devbox
35+
benchmark
36+
secret
3537
blueprint
38+
storage-object
39+
network-policy
3640
main
3741
snapshot
3842
config

src/commands/menu.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { processUtils } from "../utils/processUtils.js";
99

1010
import { Router } from "../router/Router.js";
1111
import { NavigationProvider } from "../store/navigationStore.js";
12+
import { BetaFeatureProvider } from "../store/betaFeatureStore.js";
1213
import type { ScreenName } from "../store/navigationStore.js";
1314

1415
function AppInner() {
@@ -25,12 +26,14 @@ function App({
2526
focusDevboxId?: string;
2627
}) {
2728
return (
28-
<NavigationProvider
29-
initialScreen={initialScreen}
30-
initialParams={focusDevboxId ? { focusDevboxId } : {}}
31-
>
32-
<AppInner />
33-
</NavigationProvider>
29+
<BetaFeatureProvider>
30+
<NavigationProvider
31+
initialScreen={initialScreen}
32+
initialParams={focusDevboxId ? { focusDevboxId } : {}}
33+
>
34+
<AppInner />
35+
</NavigationProvider>
36+
</BetaFeatureProvider>
3437
);
3538
}
3639

src/components/BenchmarkMenu.tsx

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* BenchmarkMenu - Sub-menu for benchmark-related resources
3+
*/
4+
import React from "react";
5+
import { Box, Text, useInput, useApp, useStdout } from "ink";
6+
import figures from "figures";
7+
import { Breadcrumb } from "./Breadcrumb.js";
8+
import { NavigationTips } from "./NavigationTips.js";
9+
import { colors } from "../utils/theme.js";
10+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
11+
12+
interface BenchmarkMenuItem {
13+
key: string;
14+
label: string;
15+
description: string;
16+
icon: string;
17+
color: string;
18+
}
19+
20+
const benchmarkMenuItems: BenchmarkMenuItem[] = [
21+
{
22+
key: "benchmark-runs",
23+
label: "Benchmark Runs",
24+
description: "View and manage benchmark executions",
25+
icon: "▶",
26+
color: colors.success,
27+
},
28+
{
29+
key: "scenario-runs",
30+
label: "Scenario Runs",
31+
description: "View individual scenario results",
32+
icon: "◈",
33+
color: colors.info,
34+
},
35+
];
36+
37+
interface BenchmarkMenuProps {
38+
onSelect: (key: string) => void;
39+
onBack: () => void;
40+
}
41+
42+
export const BenchmarkMenu = ({ onSelect, onBack }: BenchmarkMenuProps) => {
43+
const { exit } = useApp();
44+
const [selectedIndex, setSelectedIndex] = React.useState(0);
45+
const { stdout } = useStdout();
46+
47+
// Get terminal dimensions for responsive layout
48+
const getTerminalDimensions = React.useCallback(() => {
49+
return {
50+
height: stdout?.rows && stdout.rows > 0 ? stdout.rows : 20,
51+
width: stdout?.columns && stdout.columns > 0 ? stdout.columns : 80,
52+
};
53+
}, [stdout]);
54+
55+
const [terminalDimensions, setTerminalDimensions] = React.useState(
56+
getTerminalDimensions,
57+
);
58+
59+
React.useEffect(() => {
60+
setTerminalDimensions(getTerminalDimensions());
61+
62+
if (!stdout) return;
63+
64+
const handleResize = () => {
65+
setTerminalDimensions(getTerminalDimensions());
66+
};
67+
68+
stdout.on("resize", handleResize);
69+
70+
return () => {
71+
stdout.off("resize", handleResize);
72+
};
73+
}, [stdout, getTerminalDimensions]);
74+
75+
const terminalWidth = terminalDimensions.width;
76+
const isNarrow = terminalWidth < 70;
77+
78+
// Handle Ctrl+C to exit
79+
useExitOnCtrlC();
80+
81+
useInput((input, key) => {
82+
if (key.upArrow && selectedIndex > 0) {
83+
setSelectedIndex(selectedIndex - 1);
84+
} else if (key.downArrow && selectedIndex < benchmarkMenuItems.length - 1) {
85+
setSelectedIndex(selectedIndex + 1);
86+
} else if (key.return) {
87+
onSelect(benchmarkMenuItems[selectedIndex].key);
88+
} else if (key.escape) {
89+
onBack();
90+
} else if (input === "b" || input === "1") {
91+
onSelect("benchmark-runs");
92+
} else if (input === "s" || input === "2") {
93+
onSelect("scenario-runs");
94+
} else if (input === "q") {
95+
exit();
96+
}
97+
});
98+
99+
return (
100+
<Box flexDirection="column">
101+
<Breadcrumb
102+
items={[{ label: "Home" }, { label: "Benchmarks", active: true }]}
103+
/>
104+
105+
<Box paddingX={2} marginBottom={1}>
106+
<Text color={colors.primary} bold>
107+
Benchmarks
108+
</Text>
109+
<Text color={colors.textDim} dimColor>
110+
{isNarrow ? "" : " • Performance testing and evaluation"}
111+
</Text>
112+
</Box>
113+
114+
<Box flexDirection="column" paddingX={2}>
115+
{benchmarkMenuItems.map((item, index) => {
116+
const isSelected = index === selectedIndex;
117+
return (
118+
<Box key={item.key} marginBottom={0}>
119+
<Text color={isSelected ? item.color : colors.textDim}>
120+
{isSelected ? figures.pointer : " "}
121+
</Text>
122+
<Text> </Text>
123+
<Text color={item.color} bold>
124+
{item.icon}
125+
</Text>
126+
<Text> </Text>
127+
<Text
128+
color={isSelected ? item.color : colors.text}
129+
bold={isSelected}
130+
>
131+
{item.label}
132+
</Text>
133+
{!isNarrow && (
134+
<Text color={colors.textDim} dimColor>
135+
{" "}
136+
- {item.description}
137+
</Text>
138+
)}
139+
<Text color={colors.textDim} dimColor>
140+
{" "}
141+
[{index + 1}]
142+
</Text>
143+
</Box>
144+
);
145+
})}
146+
</Box>
147+
148+
<NavigationTips
149+
showArrows
150+
paddingX={2}
151+
tips={[
152+
{ key: "1-2", label: "Quick select" },
153+
{ key: "Enter", label: "Select" },
154+
{ key: "Esc", label: "Back" },
155+
{ key: "q", label: "Quit" },
156+
]}
157+
/>
158+
</Box>
159+
);
160+
};

src/components/MainMenu.tsx

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ import { colors } from "../utils/theme.js";
99
import { execCommand } from "../utils/exec.js";
1010
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
1111
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
12+
import { useBetaFeatures } from "../store/betaFeatureStore.js";
13+
import type { BetaFeature } from "../store/betaFeatureStore.js";
1214

1315
interface MenuItem {
1416
key: string;
1517
label: string;
1618
description: string;
1719
icon: string;
1820
color: string;
21+
/** If set, this item is only shown when the specified beta feature is enabled */
22+
betaFeature?: BetaFeature;
1923
}
2024

21-
const menuItems: MenuItem[] = [
25+
const allMenuItems: MenuItem[] = [
2226
{
2327
key: "devboxes",
2428
label: "Devboxes",
@@ -47,6 +51,14 @@ const menuItems: MenuItem[] = [
4751
icon: "▤",
4852
color: colors.secondary,
4953
},
54+
{
55+
key: "benchmarks",
56+
label: "Benchmarks",
57+
description: "Performance testing and evaluation",
58+
icon: "▶",
59+
color: colors.success,
60+
betaFeature: "benchmarks",
61+
},
5062
{
5163
key: "settings",
5264
label: "Settings",
@@ -72,10 +84,29 @@ function getLayoutMode(height: number): LayoutMode {
7284
return "minimal"; // No banner + labels only
7385
}
7486

87+
// Helper component for rendering beta badge
88+
const BetaBadge = () => (
89+
<Text color={colors.warning} bold>
90+
{" "}
91+
[BETA]
92+
</Text>
93+
);
94+
7595
export const MainMenu = ({ onSelect }: MainMenuProps) => {
7696
const { exit } = useApp();
7797
const [selectedIndex, setSelectedIndex] = React.useState(0);
7898
const { stdout } = useStdout();
99+
const { isFeatureEnabled } = useBetaFeatures();
100+
101+
// Filter menu items based on beta feature flags
102+
const menuItems = React.useMemo(() => {
103+
return allMenuItems.filter((item) => {
104+
if (item.betaFeature) {
105+
return isFeatureEnabled(item.betaFeature);
106+
}
107+
return true;
108+
});
109+
}, [isFeatureEnabled]);
79110

80111
// Get raw terminal dimensions, responding to resize events
81112
// Default to 20 rows / 80 cols if we can't detect
@@ -117,6 +148,27 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
117148
// Handle Ctrl+C to exit
118149
useExitOnCtrlC();
119150

151+
// Helper to select menu item by key (if available in filtered list)
152+
const selectByKey = React.useCallback(
153+
(key: string) => {
154+
if (menuItems.some((item) => item.key === key)) {
155+
onSelect(key);
156+
}
157+
},
158+
[menuItems, onSelect],
159+
);
160+
161+
// Helper to select menu item by number (1-indexed, based on filtered list)
162+
const selectByNumber = React.useCallback(
163+
(num: number) => {
164+
const index = num - 1;
165+
if (index >= 0 && index < menuItems.length) {
166+
onSelect(menuItems[index].key);
167+
}
168+
},
169+
[menuItems, onSelect],
170+
);
171+
120172
useInput((input, key) => {
121173
if (key.upArrow && selectedIndex > 0) {
122174
setSelectedIndex(selectedIndex - 1);
@@ -126,16 +178,20 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
126178
onSelect(menuItems[selectedIndex].key);
127179
} else if (key.escape) {
128180
exit();
129-
} else if (input === "d" || input === "1") {
130-
onSelect("devboxes");
131-
} else if (input === "b" || input === "2") {
132-
onSelect("blueprints");
133-
} else if (input === "s" || input === "3") {
134-
onSelect("snapshots");
135-
} else if (input === "o" || input === "4") {
136-
onSelect("objects");
137-
} else if (input === "n" || input === "5") {
138-
onSelect("settings");
181+
} else if (input === "d") {
182+
selectByKey("devboxes");
183+
} else if (input === "b") {
184+
selectByKey("blueprints");
185+
} else if (input === "s") {
186+
selectByKey("snapshots");
187+
} else if (input === "o") {
188+
selectByKey("objects");
189+
} else if (input === "e") {
190+
selectByKey("benchmarks");
191+
} else if (input === "n") {
192+
selectByKey("settings");
193+
} else if (input >= "1" && input <= "9") {
194+
selectByNumber(parseInt(input, 10));
139195
} else if (input === "u" && updateAvailable) {
140196
// Release terminal and exec into update command (never returns)
141197
execCommand("sh", [
@@ -148,12 +204,13 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
148204
const layoutMode = getLayoutMode(terminalHeight);
149205

150206
// Navigation tips for all layouts
207+
const quickSelectRange = `1-${menuItems.length}`;
151208
const navTips = (
152209
<NavigationTips
153210
showArrows
154211
paddingX={2}
155212
tips={[
156-
{ key: "1-5", label: "Quick select" },
213+
{ key: quickSelectRange, label: "Quick select" },
157214
{ key: "Enter", label: "Select" },
158215
{ key: "Esc", label: "Quit" },
159216
{ key: "u", label: "Update", condition: !!updateAvailable },
@@ -189,6 +246,7 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
189246
>
190247
{item.label}
191248
</Text>
249+
{item.betaFeature && <BetaBadge />}
192250
<Text color={colors.textDim} dimColor>
193251
{" "}
194252
[{index + 1}]
@@ -234,6 +292,7 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
234292
>
235293
{item.label}
236294
</Text>
295+
{item.betaFeature && <BetaBadge />}
237296
<Text color={colors.textDim} dimColor>
238297
{isNarrow
239298
? ` [${index + 1}]`
@@ -285,6 +344,7 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
285344
>
286345
{item.label}
287346
</Text>
347+
{item.betaFeature && <BetaBadge />}
288348
{!isNarrow && (
289349
<Text color={colors.textDim} dimColor>
290350
{" "}
@@ -346,6 +406,7 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
346406
>
347407
{item.label}
348408
</Text>
409+
{item.betaFeature && <BetaBadge />}
349410
<Text color={colors.textDim} dimColor>
350411
{" "}
351412
[{index + 1}]
@@ -386,6 +447,7 @@ export const MainMenu = ({ onSelect }: MainMenuProps) => {
386447
>
387448
{item.label}
388449
</Text>
450+
{item.betaFeature && <BetaBadge />}
389451
<Text color={colors.textDim}> </Text>
390452
<Text color={colors.textDim} dimColor>
391453
{item.description}

0 commit comments

Comments
 (0)