@runloop/rl-cli 0.1.2 → 0.2.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.
Files changed (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -48
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +22 -111
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Box, Text, useInput, useStdout } from "ink";
3
+ import { Box, Text, useInput } from "ink";
4
4
  import figures from "figures";
5
5
  import { Header } from "./Header.js";
6
6
  import { StatusBadge } from "./StatusBadge.js";
@@ -9,6 +9,9 @@ import { Breadcrumb } from "./Breadcrumb.js";
9
9
  import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
10
10
  import { getDevboxUrl } from "../utils/url.js";
11
11
  import { colors } from "../utils/theme.js";
12
+ import { useViewportHeight } from "../hooks/useViewportHeight.js";
13
+ import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
14
+ import { getDevbox } from "../services/devboxService.js";
12
15
  // Format time ago in a succinct way
13
16
  const formatTimeAgo = (timestamp) => {
14
17
  const seconds = Math.floor((Date.now() - timestamp) / 1000);
@@ -29,13 +32,53 @@ const formatTimeAgo = (timestamp) => {
29
32
  const years = Math.floor(months / 12);
30
33
  return `${years}y ago`;
31
34
  };
32
- export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest, }) => {
33
- const { stdout } = useStdout();
35
+ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
36
+ const isMounted = React.useRef(true);
37
+ // Track mounted state
38
+ React.useEffect(() => {
39
+ isMounted.current = true;
40
+ return () => {
41
+ isMounted.current = false;
42
+ };
43
+ }, []);
44
+ // Local state for devbox data (updated by polling)
45
+ const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
34
46
  const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
35
47
  const [detailScroll, setDetailScroll] = React.useState(0);
36
48
  const [showActions, setShowActions] = React.useState(false);
37
49
  const [selectedOperation, setSelectedOperation] = React.useState(0);
38
- const selectedDevbox = initialDevbox;
50
+ // Background polling for devbox details
51
+ React.useEffect(() => {
52
+ // Skip polling if showing actions, detailed info, or not mounted
53
+ if (showActions || showDetailedInfo)
54
+ return;
55
+ const interval = setInterval(async () => {
56
+ // Only poll when not in actions/detail mode and component is mounted
57
+ if (!showActions && !showDetailedInfo && isMounted.current) {
58
+ try {
59
+ const updatedDevbox = await getDevbox(initialDevbox.id);
60
+ // Only update if still mounted
61
+ if (isMounted.current) {
62
+ setCurrentDevbox(updatedDevbox);
63
+ }
64
+ }
65
+ catch {
66
+ // Silently ignore polling errors to avoid disrupting user experience
67
+ }
68
+ }
69
+ }, 3000); // Poll every 3 seconds
70
+ return () => clearInterval(interval);
71
+ }, [initialDevbox.id, showActions, showDetailedInfo]);
72
+ // Calculate viewport for detailed info view:
73
+ // - Breadcrumb (3 lines + marginBottom): 4 lines
74
+ // - Header (title + underline + marginBottom): 3 lines
75
+ // - Status box (content + marginBottom): 2 lines
76
+ // - Content box (marginTop + border + paddingY top/bottom + border + marginBottom): 6 lines
77
+ // - Help bar (marginTop + content): 2 lines
78
+ // - Safety buffer: 1 line
79
+ // Total: 18 lines
80
+ const detailViewport = useViewportHeight({ overhead: 18, minHeight: 10 });
81
+ const selectedDevbox = currentDevbox;
39
82
  const allOperations = [
40
83
  {
41
84
  key: "logs",
@@ -123,14 +166,18 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
123
166
  return op.key === "logs" || op.key === "delete";
124
167
  })
125
168
  : allOperations;
126
- // Memoize time-based values to prevent re-rendering on every tick
127
- const formattedCreateTime = React.useMemo(() => selectedDevbox.create_time_ms
169
+ const formattedCreateTime = selectedDevbox.create_time_ms
128
170
  ? new Date(selectedDevbox.create_time_ms).toLocaleString()
129
- : "", [selectedDevbox.create_time_ms]);
130
- const createTimeAgo = React.useMemo(() => selectedDevbox.create_time_ms
171
+ : "";
172
+ const createTimeAgo = selectedDevbox.create_time_ms
131
173
  ? formatTimeAgo(selectedDevbox.create_time_ms)
132
- : "", [selectedDevbox.create_time_ms]);
174
+ : "";
175
+ // Handle Ctrl+C to exit
176
+ useExitOnCtrlC();
133
177
  useInput((input, key) => {
178
+ // Don't process input if unmounting
179
+ if (!isMounted.current)
180
+ return;
134
181
  // Skip input handling when in actions view
135
182
  if (showActions) {
136
183
  return;
@@ -161,7 +208,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
161
208
  }
162
209
  // Main view input handling
163
210
  if (input === "q" || key.escape) {
164
- console.clear();
165
211
  onBack();
166
212
  }
167
213
  else if (input === "i") {
@@ -175,7 +221,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
175
221
  setSelectedOperation(selectedOperation + 1);
176
222
  }
177
223
  else if (key.return || input === "a") {
178
- console.clear();
179
224
  setShowActions(true);
180
225
  }
181
226
  else if (input) {
@@ -183,7 +228,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
183
228
  const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
184
229
  if (matchedOpIndex !== -1) {
185
230
  setSelectedOperation(matchedOpIndex);
186
- console.clear();
187
231
  setShowActions(true);
188
232
  }
189
233
  }
@@ -217,10 +261,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
217
261
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
218
262
  // Core Information
219
263
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Devbox Details" }, "core-title"));
220
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "ID: ", selectedDevbox.id] }, "core-id"));
264
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.id] }, "core-id"));
221
265
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", selectedDevbox.name || "(none)"] }, "core-name"));
222
266
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(selectedDevbox.status)] }, "core-status"));
223
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(selectedDevbox.create_time_ms).toLocaleString()] }, "core-created"));
267
+ if (selectedDevbox.create_time_ms) {
268
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(selectedDevbox.create_time_ms).toLocaleString()] }, "core-created"));
269
+ }
224
270
  if (selectedDevbox.end_time_ms) {
225
271
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(selectedDevbox.end_time_ms).toLocaleString()] }, "core-ended"));
226
272
  }
@@ -263,9 +309,14 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
263
309
  }
264
310
  if (lp.launch_commands && lp.launch_commands.length > 0) {
265
311
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "launch-launch-cmds"));
266
- lp.launch_commands.forEach((cmd, idx) => {
267
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `launch-cmd-${idx}`));
268
- });
312
+ // lp.launch_commands.forEach((cmd: string, idx: number) => {
313
+ // lines.push(
314
+ // <Text key={`launch-cmd-${idx}`} dimColor>
315
+ // {" "}
316
+ // {figures.pointer} {cmd}
317
+ // </Text>,
318
+ // );
319
+ // });
269
320
  }
270
321
  if (lp.required_services && lp.required_services.length > 0) {
271
322
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Required Services: ", lp.required_services.join(", ")] }, "launch-services"));
@@ -285,10 +336,10 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
285
336
  if (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) {
286
337
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
287
338
  if (selectedDevbox.blueprint_id) {
288
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Blueprint: ", selectedDevbox.blueprint_id] }, "source-bp"));
339
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.blueprint_id] }, "source-bp"));
289
340
  }
290
341
  if (selectedDevbox.snapshot_id) {
291
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Snapshot: ", selectedDevbox.snapshot_id] }, "source-snap"));
342
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.snapshot_id] }, "source-snap"));
292
343
  }
293
344
  lines.push(_jsx(Text, { children: " " }, "source-space"));
294
345
  }
@@ -297,7 +348,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
297
348
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Initiator" }, "init-title"));
298
349
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", selectedDevbox.initiator_type] }, "init-type"));
299
350
  if (selectedDevbox.initiator_id) {
300
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "ID: ", selectedDevbox.initiator_id] }, "init-id"));
351
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.initiator_id] }, "init-id"));
301
352
  }
302
353
  lines.push(_jsx(Text, { children: " " }, "init-space"));
303
354
  }
@@ -348,13 +399,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
348
399
  }, breadcrumbItems: [
349
400
  { label: "Devboxes" },
350
401
  { label: selectedDevbox.name || selectedDevbox.id },
351
- ], initialOperation: selectedOp?.key, skipOperationsMenu: true, onSSHRequest: onSSHRequest }));
402
+ ], initialOperation: selectedOp?.key, skipOperationsMenu: true }));
352
403
  }
353
404
  // Detailed info mode - full screen
354
405
  if (showDetailedInfo) {
355
406
  const detailLines = buildDetailLines();
356
- const terminalHeight = stdout?.rows || 30;
357
- const viewportHeight = Math.max(10, terminalHeight - 12); // Reserve space for header/footer
407
+ const viewportHeight = detailViewport.viewportHeight;
358
408
  const maxScroll = Math.max(0, detailLines.length - viewportHeight);
359
409
  const actualScroll = Math.min(detailScroll, maxScroll);
360
410
  const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
@@ -364,7 +414,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
364
414
  { label: "Devboxes" },
365
415
  { label: selectedDevbox.name || selectedDevbox.id },
366
416
  { label: "Full Details", active: true },
367
- ] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: selectedDevbox.id })] }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: colors.border, paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: visibleLines }), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 [q or esc] Back to Details \u2022 Line", " ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of", " ", detailLines.length] }) })] }));
417
+ ] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.id })] }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: colors.border, paddingX: 2, paddingY: 1, children: _jsx(Box, { flexDirection: "column", children: visibleLines }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of", " ", detailLines.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 [q or esc] Back to Details"] })] })] }));
368
418
  }
369
419
  // Main detail view
370
420
  const lp = selectedDevbox.launch_parameters;
@@ -374,7 +424,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
374
424
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
375
425
  { label: "Devboxes" },
376
426
  { label: selectedDevbox.name || selectedDevbox.id, active: true },
377
- ] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.success, dimColor: true, children: ["Uptime:", " ", uptime < 60
427
+ ] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: colors.idColor, children: [" \u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.success, dimColor: true, children: ["Uptime:", " ", uptime < 60
378
428
  ? `${uptime}m`
379
429
  : `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }), lp?.keep_alive_time_seconds && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Keep-alive: ", Math.floor(lp.keep_alive_time_seconds / 60), "m"] }))] }))] }), _jsxs(Box, { flexDirection: "row", gap: 1, marginBottom: 1, children: [(lp?.resource_size_request ||
380
430
  lp?.custom_cpu_cores ||
@@ -382,9 +432,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
382
432
  lp?.custom_disk_size ||
383
433
  lp?.architecture) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.info, bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
384
434
  .filter((c) => c !== "unknown")
385
- .join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), _jsxs(Text, { dimColor: true, children: [selectedDevbox.blueprint_id &&
386
- `BP: ${selectedDevbox.blueprint_id}`, selectedDevbox.snapshot_id &&
387
- `Snap: ${selectedDevbox.snapshot_id}`] })] }))] }), selectedDevbox.metadata &&
435
+ .join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), selectedDevbox.blueprint_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "BP: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.blueprint_id })] })), selectedDevbox.snapshot_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Snap: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.snapshot_id })] }))] }))] }), selectedDevbox.metadata &&
388
436
  Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
389
437
  const isSelected = index === selectedOperation;
390
438
  return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { colors } from "../utils/theme.js";
5
+ /**
6
+ * ErrorBoundary to catch and handle React errors gracefully
7
+ * Particularly useful for catching Yoga WASM layout errors
8
+ */
9
+ export class ErrorBoundary extends React.Component {
10
+ constructor(props) {
11
+ super(props);
12
+ this.state = { hasError: false };
13
+ }
14
+ static getDerivedStateFromError(error) {
15
+ return { hasError: true, error };
16
+ }
17
+ componentDidCatch(error, errorInfo) {
18
+ console.error("ErrorBoundary caught an error:", error, errorInfo);
19
+ }
20
+ render() {
21
+ if (this.state.hasError) {
22
+ if (this.props.fallback) {
23
+ return this.props.fallback;
24
+ }
25
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u26A0\uFE0F Rendering Error" }), _jsx(Text, { color: colors.textDim, children: this.state.error?.message || "An unexpected error occurred" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Ctrl+C to exit" }) })] }));
26
+ }
27
+ return this.props.children;
28
+ }
29
+ }
@@ -2,6 +2,14 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import figures from "figures";
4
4
  import { colors } from "../utils/theme.js";
5
- export const ErrorMessage = ({ message, error, }) => {
6
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " ", message] }) }), error && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: error.message }) }))] }));
5
+ export const ErrorMessage = ({ message, error }) => {
6
+ // Limit message length to prevent Yoga layout engine errors
7
+ const MAX_LENGTH = 500;
8
+ const truncatedMessage = message.length > MAX_LENGTH
9
+ ? message.substring(0, MAX_LENGTH) + "..."
10
+ : message;
11
+ const truncatedError = error?.message && error.message.length > MAX_LENGTH
12
+ ? error.message.substring(0, MAX_LENGTH) + "..."
13
+ : error?.message;
14
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " ", truncatedMessage] }) }), truncatedError && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: truncatedError }) }))] }));
7
15
  };
@@ -1,7 +1,15 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
2
  import { Box, Text } from "ink";
4
3
  import { colors } from "../utils/theme.js";
5
- export const Header = React.memo(({ title, subtitle }) => {
6
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: colors.accent3, children: ["\u258C", title] }), subtitle && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: subtitle })] }))] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.accent3, children: "─".repeat(title.length + 1) }) })] }));
7
- });
4
+ export const Header = ({ title, subtitle }) => {
5
+ // Limit lengths to prevent Yoga layout engine errors
6
+ const MAX_TITLE_LENGTH = 100;
7
+ const MAX_SUBTITLE_LENGTH = 150;
8
+ const truncatedTitle = title.length > MAX_TITLE_LENGTH
9
+ ? title.substring(0, MAX_TITLE_LENGTH) + "..."
10
+ : title;
11
+ const truncatedSubtitle = subtitle && subtitle.length > MAX_SUBTITLE_LENGTH
12
+ ? subtitle.substring(0, MAX_SUBTITLE_LENGTH) + "..."
13
+ : subtitle;
14
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: colors.accent3, children: ["\u258C", truncatedTitle] }), truncatedSubtitle && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: truncatedSubtitle })] }))] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.accent3, children: "─".repeat(Math.max(0, Math.floor(Math.min(truncatedTitle.length + 1, MAX_TITLE_LENGTH + 1)))) }) })] }));
15
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * InteractiveSpawn - Custom component for running interactive subprocesses
3
+ * Based on Ink's subprocess-output example pattern
4
+ * Handles proper TTY allocation for interactive commands like SSH
5
+ */
6
+ import React from "react";
7
+ import { spawn } from "child_process";
8
+ import { exitAlternateScreenBuffer, enterAlternateScreenBuffer, } from "../utils/screen.js";
9
+ /**
10
+ * Releases terminal control from Ink so a subprocess can take over.
11
+ * This directly manipulates stdin to bypass Ink's input handling.
12
+ */
13
+ function releaseTerminal() {
14
+ // Pause stdin to stop Ink from reading input
15
+ process.stdin.pause();
16
+ // Disable raw mode so the subprocess can control terminal echo and line buffering
17
+ // SSH needs to set its own terminal modes
18
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
19
+ process.stdin.setRawMode(false);
20
+ }
21
+ }
22
+ /**
23
+ * Restores terminal control to Ink after subprocess exits.
24
+ */
25
+ function restoreTerminal() {
26
+ // Re-enable raw mode for Ink's input handling
27
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
28
+ process.stdin.setRawMode(true);
29
+ }
30
+ // Resume stdin so Ink can read input again
31
+ process.stdin.resume();
32
+ }
33
+ export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
34
+ const processRef = React.useRef(null);
35
+ const hasSpawnedRef = React.useRef(false);
36
+ // Use a stable string representation of args for dependency comparison
37
+ const argsKey = React.useMemo(() => JSON.stringify(args), [args]);
38
+ React.useEffect(() => {
39
+ // Only spawn once - prevent re-spawning if component re-renders
40
+ if (hasSpawnedRef.current) {
41
+ return;
42
+ }
43
+ hasSpawnedRef.current = true;
44
+ // Exit alternate screen so subprocess gets a clean terminal
45
+ exitAlternateScreenBuffer();
46
+ // Release terminal from Ink's control
47
+ releaseTerminal();
48
+ // Small delay to ensure terminal state is fully released
49
+ setTimeout(() => {
50
+ // Spawn the process with inherited stdio for proper TTY allocation
51
+ const child = spawn(command, args, {
52
+ stdio: "inherit", // This allows the process to use the terminal directly
53
+ shell: false,
54
+ });
55
+ processRef.current = child;
56
+ // Handle process exit
57
+ child.on("exit", (code, _signal) => {
58
+ processRef.current = null;
59
+ hasSpawnedRef.current = false;
60
+ // Restore terminal control to Ink
61
+ restoreTerminal();
62
+ // Re-enter alternate screen after process exits
63
+ enterAlternateScreenBuffer();
64
+ if (onExit) {
65
+ onExit(code);
66
+ }
67
+ });
68
+ // Handle spawn errors
69
+ child.on("error", (error) => {
70
+ processRef.current = null;
71
+ hasSpawnedRef.current = false;
72
+ // Restore terminal control to Ink
73
+ restoreTerminal();
74
+ // Re-enter alternate screen on error
75
+ enterAlternateScreenBuffer();
76
+ if (onError) {
77
+ onError(error);
78
+ }
79
+ });
80
+ }, 50);
81
+ // Cleanup function - kill the process if component unmounts
82
+ return () => {
83
+ if (processRef.current && !processRef.current.killed) {
84
+ processRef.current.kill("SIGTERM");
85
+ }
86
+ // Restore terminal state on cleanup
87
+ restoreTerminal();
88
+ hasSpawnedRef.current = false;
89
+ };
90
+ }, [command, argsKey, onExit, onError]);
91
+ // This component doesn't render anything - it just manages the subprocess
92
+ // The subprocess output goes directly to the terminal via stdio: "inherit"
93
+ return null;
94
+ };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text, useInput, useApp } from "ink";
4
4
  import figures from "figures";
@@ -6,34 +6,38 @@ import { Banner } from "./Banner.js";
6
6
  import { Breadcrumb } from "./Breadcrumb.js";
7
7
  import { VERSION } from "../cli.js";
8
8
  import { colors } from "../utils/theme.js";
9
- export const MainMenu = React.memo(({ onSelect }) => {
9
+ import { useViewportHeight } from "../hooks/useViewportHeight.js";
10
+ import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
11
+ const menuItems = [
12
+ {
13
+ key: "devboxes",
14
+ label: "Devboxes",
15
+ description: "Manage cloud development environments",
16
+ icon: "◉",
17
+ color: colors.accent1,
18
+ },
19
+ {
20
+ key: "blueprints",
21
+ label: "Blueprints",
22
+ description: "Create and manage devbox templates",
23
+ icon: "▣",
24
+ color: colors.accent2,
25
+ },
26
+ {
27
+ key: "snapshots",
28
+ label: "Snapshots",
29
+ description: "Save and restore devbox states",
30
+ icon: "◈",
31
+ color: colors.accent3,
32
+ },
33
+ ];
34
+ export const MainMenu = ({ onSelect }) => {
10
35
  const { exit } = useApp();
11
36
  const [selectedIndex, setSelectedIndex] = React.useState(0);
12
- // Calculate terminal height once at mount and memoize
13
- const terminalHeight = React.useMemo(() => process.stdout.rows || 24, []);
14
- const menuItems = React.useMemo(() => [
15
- {
16
- key: "devboxes",
17
- label: "Devboxes",
18
- description: "Manage cloud development environments",
19
- icon: "◉",
20
- color: colors.accent1,
21
- },
22
- {
23
- key: "blueprints",
24
- label: "Blueprints",
25
- description: "Create and manage devbox templates",
26
- icon: "▣",
27
- color: colors.accent2,
28
- },
29
- {
30
- key: "snapshots",
31
- label: "Snapshots",
32
- description: "Save and restore devbox states",
33
- icon: "◈",
34
- color: colors.accent3,
35
- },
36
- ], []);
37
+ // Use centralized viewport hook for consistent layout
38
+ const { terminalHeight } = useViewportHeight({ overhead: 0 });
39
+ // Handle Ctrl+C to exit
40
+ useExitOnCtrlC();
37
41
  useInput((input, key) => {
38
42
  if (key.upArrow && selectedIndex > 0) {
39
43
  setSelectedIndex(selectedIndex - 1);
@@ -58,15 +62,15 @@ export const MainMenu = React.memo(({ onSelect }) => {
58
62
  }
59
63
  });
60
64
  // Use compact layout if terminal height is less than 20 lines (memoized)
61
- const useCompactLayout = React.useMemo(() => terminalHeight < 20, [terminalHeight]);
65
+ const useCompactLayout = terminalHeight < 20;
62
66
  if (useCompactLayout) {
63
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Cloud development environments \u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
67
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Cloud development environments \u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
64
68
  const isSelected = index === selectedIndex;
65
69
  return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
66
70
  }) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
67
71
  }
68
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsx(Box, { flexShrink: 0, children: _jsx(Banner, {}) }), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
72
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }] }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
69
73
  const isSelected = index === selectedIndex;
70
- return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: isSelected ? "round" : "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [_jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
74
+ return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: figures.pointer }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
71
75
  })] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
72
- });
76
+ };
@@ -1,8 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { Badge } from "@inkjs/ui";
4
3
  import figures from "figures";
5
4
  import { colors } from "../utils/theme.js";
5
+ const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: ": " }), _jsx(Text, { color: color, children: value })] }));
6
6
  // Generate color for each key based on hash
7
7
  const getColorForKey = (key, index) => {
8
8
  const colorList = [
@@ -20,10 +20,10 @@ export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = fal
20
20
  if (entries.length === 0) {
21
21
  return null;
22
22
  }
23
- const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.info, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
23
+ const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 1, children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
24
24
  const color = getColorForKey(key, index);
25
25
  const isSelected = selectedKey === key;
26
- return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })), _jsx(Badge, { color: isSelected ? colors.primary : color, children: `${key}: ${value}` })] }, key));
26
+ return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })), renderKeyValueBadge(key, value, isSelected ? colors.primary : color)] }, key));
27
27
  })] }));
28
28
  if (showBorder) {
29
29
  return (_jsx(Box, { borderStyle: "round", borderColor: colors.accent3, paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
@@ -6,7 +6,7 @@ import { colors } from "../utils/theme.js";
6
6
  * Reusable operations menu component for detail pages
7
7
  * Displays a list of available operations with keyboard navigation
8
8
  */
9
- export const OperationsMenu = ({ operations, selectedIndex, onNavigate, onSelect, onBack, additionalActions = [], }) => {
9
+ export const OperationsMenu = ({ operations, selectedIndex, onNavigate: _onNavigate, onSelect: _onSelect, onBack: _onBack, additionalActions = [], }) => {
10
10
  return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
11
11
  const isSelected = index === selectedIndex;
12
12
  return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] })] }, op.key));
@@ -8,8 +8,8 @@ import { ActionsPopup } from "./ActionsPopup.js";
8
8
  import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
9
9
  export const ResourceActionsMenu = (props) => {
10
10
  if (props.resourceType === "devbox") {
11
- const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu, onSSHRequest, } = props;
12
- return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu, onSSHRequest: onSSHRequest }));
11
+ const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu, } = props;
12
+ return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu }));
13
13
  }
14
14
  // Blueprint generic actions menu
15
15
  const { resource, onBack, breadcrumbItems = [
@@ -101,10 +101,10 @@ export const ResourceActionsMenu = (props) => {
101
101
  // Screens
102
102
  if (operationResult || operationError) {
103
103
  const label = operations.find((o) => o.key === executingOperation)?.label;
104
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
104
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
105
105
  }
106
106
  if (executingOperation && selectedOp?.needsInput) {
107
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
107
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
108
108
  }
109
109
  // Operations menu
110
110
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: resource, operations: operations.map((op) => ({