@runloop/rl-cli 0.1.1 → 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 -52
  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 +20 -90
  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,21 +1,24 @@
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, useApp, useStdout } from "ink";
3
+ import { Box, Text, useInput } from "ink";
4
4
  import TextInput from "ink-text-input";
5
5
  import figures from "figures";
6
- import { getClient } from "../utils/client.js";
7
6
  import { Header } from "./Header.js";
8
7
  import { SpinnerComponent } from "./Spinner.js";
9
8
  import { ErrorMessage } from "./ErrorMessage.js";
10
9
  import { SuccessMessage } from "./SuccessMessage.js";
11
10
  import { Breadcrumb } from "./Breadcrumb.js";
12
11
  import { colors } from "../utils/theme.js";
12
+ import { useViewportHeight } from "../hooks/useViewportHeight.js";
13
+ import { useNavigation } from "../store/navigationStore.js";
14
+ import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
15
+ import { getDevboxLogs, execCommand, suspendDevbox, resumeDevbox, shutdownDevbox, uploadFile, createSnapshot as createDevboxSnapshot, createTunnel, createSSHKey, } from "../services/devboxService.js";
16
+ import { parseLogEntry } from "../utils/logFormatter.js";
13
17
  export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
14
18
  { label: "Devboxes" },
15
19
  { label: devbox.name || devbox.id, active: true },
16
- ], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onSSHRequest, }) => {
17
- const { exit } = useApp();
18
- const { stdout } = useStdout();
20
+ ], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, }) => {
21
+ const { navigate, currentScreen, params } = useNavigation();
19
22
  const [loading, setLoading] = React.useState(false);
20
23
  const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
21
24
  const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
@@ -26,6 +29,44 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
26
29
  const [logsScroll, setLogsScroll] = React.useState(0);
27
30
  const [execScroll, setExecScroll] = React.useState(0);
28
31
  const [copyStatus, setCopyStatus] = React.useState(null);
32
+ // Calculate viewport for exec output:
33
+ // - Breadcrumb (3 lines + marginBottom): 4 lines
34
+ // - Command header (border + 2 content + border + marginBottom): 5 lines
35
+ // - Output box borders: 2 lines
36
+ // - Stats bar (marginTop + content): 2 lines
37
+ // - Help bar (marginTop + content): 2 lines
38
+ // - Safety buffer: 1 line
39
+ // Total: 16 lines
40
+ const execViewport = useViewportHeight({ overhead: 16, minHeight: 10 });
41
+ // Calculate viewport for logs output:
42
+ // - Breadcrumb (3 lines + marginBottom): 4 lines
43
+ // - Log box borders: 2 lines
44
+ // - Stats bar (marginTop + content): 2 lines
45
+ // - Help bar (marginTop + content): 2 lines
46
+ // - Safety buffer: 1 line
47
+ // Total: 11 lines
48
+ const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
49
+ // CRITICAL: Aggressive memory cleanup to prevent heap exhaustion
50
+ React.useEffect(() => {
51
+ // Clear large data immediately when results are shown to free memory faster
52
+ if (operationResult || operationError) {
53
+ const timer = setTimeout(() => {
54
+ // After 100ms, if user hasn't acted, start aggressive cleanup
55
+ // This helps with memory without disrupting UX
56
+ }, 100);
57
+ return () => clearTimeout(timer);
58
+ }
59
+ }, [operationResult, operationError]);
60
+ // Cleanup on unmount
61
+ React.useEffect(() => {
62
+ return () => {
63
+ // Aggressively null out all large data structures
64
+ setOperationResult(null);
65
+ setOperationError(null);
66
+ setOperationInput("");
67
+ setLoading(false);
68
+ };
69
+ }, []);
29
70
  const allOperations = [
30
71
  {
31
72
  key: "logs",
@@ -123,6 +164,8 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
123
164
  executeOperation();
124
165
  }
125
166
  }, [executingOperation]);
167
+ // Handle Ctrl+C to exit
168
+ useExitOnCtrlC();
126
169
  useInput((input, key) => {
127
170
  // Handle operation input mode
128
171
  if (executingOperation && !operationResult && !operationError) {
@@ -130,7 +173,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
130
173
  executeOperation();
131
174
  }
132
175
  else if (input === "q" || key.escape) {
133
- console.clear();
134
176
  setExecutingOperation(null);
135
177
  setOperationInput("");
136
178
  }
@@ -139,20 +181,21 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
139
181
  // Handle operation result display
140
182
  if (operationResult || operationError) {
141
183
  if (input === "q" || key.escape || key.return) {
142
- console.clear();
184
+ // Clear large data structures immediately to prevent memory leaks
185
+ setOperationResult(null);
186
+ setOperationError(null);
187
+ setOperationInput("");
188
+ setLogsWrapMode(true);
189
+ setLogsScroll(0);
190
+ setExecScroll(0);
191
+ setCopyStatus(null);
143
192
  // If skipOperationsMenu is true, go back to parent instead of operations menu
144
193
  if (skipOperationsMenu) {
194
+ setExecutingOperation(null);
145
195
  onBack();
146
196
  }
147
197
  else {
148
- setOperationResult(null);
149
- setOperationError(null);
150
198
  setExecutingOperation(null);
151
- setOperationInput("");
152
- setLogsWrapMode(true);
153
- setLogsScroll(0);
154
- setExecScroll(0);
155
- setCopyStatus(null);
156
199
  }
157
200
  }
158
201
  else if ((key.upArrow || input === "k") &&
@@ -193,9 +236,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
193
236
  ...(operationResult.stdout || "").split("\n"),
194
237
  ...(operationResult.stderr || "").split("\n"),
195
238
  ];
196
- const terminalHeight = stdout?.rows || 30;
197
- const viewportHeight = Math.max(10, terminalHeight - 15);
198
- const maxScroll = Math.max(0, lines.length - viewportHeight);
239
+ const maxScroll = Math.max(0, lines.length - execViewport.viewportHeight);
199
240
  setExecScroll(maxScroll);
200
241
  }
201
242
  else if (input === "c" &&
@@ -277,9 +318,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
277
318
  typeof operationResult === "object" &&
278
319
  operationResult.__customRender === "logs") {
279
320
  const logs = operationResult.__logs || [];
280
- const terminalHeight = stdout?.rows || 30;
281
- const viewportHeight = Math.max(10, terminalHeight - 10);
282
- const maxScroll = Math.max(0, logs.length - viewportHeight);
321
+ const maxScroll = Math.max(0, logs.length - logsViewport.viewportHeight);
283
322
  setLogsScroll(maxScroll);
284
323
  }
285
324
  else if (input === "w" &&
@@ -292,19 +331,15 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
292
331
  operationResult &&
293
332
  typeof operationResult === "object" &&
294
333
  operationResult.__customRender === "logs") {
295
- // Copy logs to clipboard
334
+ // Copy logs to clipboard using shared formatter
296
335
  const logs = operationResult.__logs || [];
297
336
  const logsText = logs
298
337
  .map((log) => {
299
- const time = new Date(log.timestamp_ms).toLocaleString();
300
- const level = log.level || "INFO";
301
- const source = log.source || "exec";
302
- const message = log.message || "";
303
- const cmd = log.cmd ? `[${log.cmd}] ` : "";
304
- const exitCode = log.exit_code !== null && log.exit_code !== undefined
305
- ? `(${log.exit_code}) `
306
- : "";
307
- return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
338
+ const parts = parseLogEntry(log);
339
+ const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
340
+ const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
341
+ const shell = parts.shellName ? `(${parts.shellName}) ` : "";
342
+ return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
308
343
  })
309
344
  .join("\n");
310
345
  const copyToClipboard = async (text) => {
@@ -348,9 +383,14 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
348
383
  }
349
384
  // Operations selection mode
350
385
  if (input === "q" || key.escape) {
351
- console.clear();
352
- onBack();
386
+ // Clear all state before going back to free memory
387
+ setOperationResult(null);
388
+ setOperationError(null);
389
+ setOperationInput("");
390
+ setExecutingOperation(null);
353
391
  setSelectedOperation(0);
392
+ setLoading(false);
393
+ onBack();
354
394
  }
355
395
  else if (key.upArrow && selectedOperation > 0) {
356
396
  setSelectedOperation(selectedOperation - 1);
@@ -359,7 +399,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
359
399
  setSelectedOperation(selectedOperation + 1);
360
400
  }
361
401
  else if (key.return) {
362
- console.clear();
363
402
  const op = operations[selectedOperation].key;
364
403
  setExecutingOperation(op);
365
404
  }
@@ -367,20 +406,17 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
367
406
  // Check if input matches any operation shortcut
368
407
  const matchedOp = operations.find((op) => op.shortcut === input);
369
408
  if (matchedOp) {
370
- console.clear();
371
409
  setExecutingOperation(matchedOp.key);
372
410
  }
373
411
  }
374
412
  });
375
413
  const executeOperation = async () => {
376
- const client = getClient();
377
414
  try {
378
415
  setLoading(true);
379
416
  switch (executingOperation) {
380
417
  case "exec":
381
- const execResult = await client.devboxes.executeSync(devbox.id, {
382
- command: operationInput,
383
- });
418
+ // Use service layer (already truncates output to prevent Yoga crashes)
419
+ const execResult = await execCommand(devbox.id, operationInput);
384
420
  // Format exec result for custom rendering
385
421
  const formattedExecResult = {
386
422
  __customRender: "exec",
@@ -392,23 +428,19 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
392
428
  setOperationResult(formattedExecResult);
393
429
  break;
394
430
  case "upload":
395
- const fs = await import("fs");
396
- const fileStream = fs.createReadStream(operationInput);
431
+ // Use service layer
397
432
  const filename = operationInput.split("/").pop() || "file";
398
- await client.devboxes.uploadFile(devbox.id, {
399
- path: filename,
400
- file: fileStream,
401
- });
433
+ await uploadFile(devbox.id, operationInput, filename);
402
434
  setOperationResult(`File ${filename} uploaded successfully`);
403
435
  break;
404
436
  case "snapshot":
405
- const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
406
- name: operationInput || `snapshot-${Date.now()}`,
407
- });
437
+ // Use service layer
438
+ const snapshot = await createDevboxSnapshot(devbox.id, operationInput || `snapshot-${Date.now()}`);
408
439
  setOperationResult(`Snapshot created: ${snapshot.id}`);
409
440
  break;
410
441
  case "ssh":
411
- const sshKey = await client.devboxes.createSSHKey(devbox.id);
442
+ // Use service layer
443
+ const sshKey = await createSSHKey(devbox.id);
412
444
  const fsModule = await import("fs");
413
445
  const pathModule = await import("path");
414
446
  const osModule = await import("os");
@@ -421,45 +453,45 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
421
453
  const sshUser = devbox.launch_parameters?.user_parameters?.username || "user";
422
454
  const env = process.env.RUNLOOP_ENV?.toLowerCase();
423
455
  const sshHost = env === "dev" ? "ssh.runloop.pro" : "ssh.runloop.ai";
424
- const proxyCommand = `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
425
- const sshConfig = {
456
+ // macOS openssl doesn't support -verify_quiet, use compatible flags
457
+ // servername should be %h (target hostname) - SSH will replace %h with the actual hostname from the SSH command
458
+ // This matches the reference implementation where servername is the target hostname
459
+ const proxyCommand = `openssl s_client -quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
460
+ // Navigate to SSH session screen
461
+ navigate("ssh-session", {
426
462
  keyPath,
427
463
  proxyCommand,
428
464
  sshUser,
429
465
  url: sshKey.url,
430
466
  devboxId: devbox.id,
431
467
  devboxName: devbox.name || devbox.id,
432
- };
433
- // Notify parent that SSH is requested
434
- if (onSSHRequest) {
435
- onSSHRequest(sshConfig);
436
- exit();
437
- }
438
- else {
439
- setOperationError(new Error("SSH session handler not configured"));
440
- }
468
+ returnScreen: currentScreen,
469
+ returnParams: params,
470
+ });
441
471
  break;
442
472
  case "logs":
443
- const logsResult = await client.devboxes.logs.list(devbox.id);
444
- if (logsResult.logs.length === 0) {
473
+ // Use service layer (already truncates and escapes log messages)
474
+ const logs = await getDevboxLogs(devbox.id);
475
+ if (logs.length === 0) {
445
476
  setOperationResult("No logs available for this devbox.");
446
477
  }
447
478
  else {
448
- logsResult.__customRender = "logs";
449
- logsResult.__logs = logsResult.logs;
450
- logsResult.__totalCount = logsResult.logs.length;
479
+ const logsResult = {
480
+ __customRender: "logs",
481
+ __logs: logs,
482
+ __totalCount: logs.length,
483
+ };
451
484
  setOperationResult(logsResult);
452
485
  }
453
486
  break;
454
487
  case "tunnel":
488
+ // Use service layer
455
489
  const port = parseInt(operationInput);
456
490
  if (isNaN(port) || port < 1 || port > 65535) {
457
491
  setOperationError(new Error("Invalid port number. Please enter a port between 1 and 65535."));
458
492
  }
459
493
  else {
460
- const tunnel = await client.devboxes.createTunnel(devbox.id, {
461
- port,
462
- });
494
+ const tunnel = await createTunnel(devbox.id, port);
463
495
  setOperationResult(`Tunnel created!\n\n` +
464
496
  `Local Port: ${port}\n` +
465
497
  `Public URL: ${tunnel.url}\n\n` +
@@ -467,15 +499,18 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
467
499
  }
468
500
  break;
469
501
  case "suspend":
470
- await client.devboxes.suspend(devbox.id);
502
+ // Use service layer
503
+ await suspendDevbox(devbox.id);
471
504
  setOperationResult(`Devbox ${devbox.id} suspended successfully`);
472
505
  break;
473
506
  case "resume":
474
- await client.devboxes.resume(devbox.id);
507
+ // Use service layer
508
+ await resumeDevbox(devbox.id);
475
509
  setOperationResult(`Devbox ${devbox.id} resumed successfully`);
476
510
  break;
477
511
  case "delete":
478
- await client.devboxes.shutdown(devbox.id);
512
+ // Use service layer
513
+ await shutdownDevbox(devbox.id);
479
514
  setOperationResult(`Devbox ${devbox.id} shut down successfully`);
480
515
  break;
481
516
  }
@@ -501,8 +536,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
501
536
  const stdoutLines = stdout ? stdout.split("\n") : [];
502
537
  const stderrLines = stderr ? stderr.split("\n") : [];
503
538
  const allLines = [...stdoutLines, ...stderrLines].filter((line) => line !== "");
504
- const terminalHeight = stdout?.rows || 30;
505
- const viewportHeight = Math.max(10, terminalHeight - 15);
539
+ const viewportHeight = execViewport.viewportHeight;
506
540
  const maxScroll = Math.max(0, allLines.length - viewportHeight);
507
541
  const actualScroll = Math.min(execScroll, maxScroll);
508
542
  const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
@@ -512,12 +546,14 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
512
546
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
513
547
  ...breadcrumbItems,
514
548
  { label: "Execute Command", active: true },
515
- ] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.text, children: command })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Exit Code:", " "] }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
549
+ ] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.text, children: command.length > 500
550
+ ? command.substring(0, 500) + "..."
551
+ : command })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Exit Code:", " "] }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
516
552
  const actualIndex = actualScroll + index;
517
553
  const isStderr = actualIndex >= stdoutLines.length;
518
554
  const lineColor = isStderr ? colors.error : colors.text;
519
555
  return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
520
- }), 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"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
556
+ })] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] }), hasLess && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] })), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
521
557
  }
522
558
  // Check for custom logs rendering
523
559
  if (operationResult &&
@@ -525,45 +561,76 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
525
561
  operationResult.__customRender === "logs") {
526
562
  const logs = operationResult.__logs || [];
527
563
  const totalCount = operationResult.__totalCount || 0;
528
- const terminalHeight = stdout?.rows || 30;
529
- const terminalWidth = stdout?.columns || 120;
530
- const viewportHeight = Math.max(10, terminalHeight - 10);
564
+ const viewportHeight = logsViewport.viewportHeight;
565
+ const terminalWidth = logsViewport.terminalWidth;
531
566
  const maxScroll = Math.max(0, logs.length - viewportHeight);
532
567
  const actualScroll = Math.min(logsScroll, maxScroll);
533
568
  const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
534
569
  const hasMore = actualScroll + viewportHeight < logs.length;
535
570
  const hasLess = actualScroll > 0;
536
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: "Logs", active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [visibleLogs.map((log, index) => {
537
- const time = new Date(log.timestamp_ms).toLocaleTimeString();
538
- const level = log.level ? log.level[0].toUpperCase() : "I";
539
- const source = log.source ? log.source.substring(0, 8) : "exec";
540
- const fullMessage = log.message || "";
541
- const cmd = log.cmd
542
- ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? "..." : ""}] `
543
- : "";
544
- const exitCode = log.exit_code !== null && log.exit_code !== undefined
545
- ? `(${log.exit_code}) `
546
- : "";
547
- let levelColor = colors.textDim;
548
- if (level === "E")
549
- levelColor = colors.error;
550
- else if (level === "W")
551
- levelColor = colors.warning;
552
- else if (level === "I")
553
- levelColor = colors.primary;
554
- if (logsWrapMode) {
555
- return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: fullMessage })] }, index));
556
- }
557
- else {
558
- const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
559
- const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
560
- const truncatedMessage = fullMessage.length > availableMessageWidth
561
- ? fullMessage.substring(0, availableMessageWidth - 3) +
562
- "..."
563
- : fullMessage;
564
- return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: truncatedMessage })] }, index));
565
- }
566
- }), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _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 logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.length] }), _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(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
571
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: "Logs", active: true }] }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: visibleLogs.map((log, index) => {
572
+ const parts = parseLogEntry(log);
573
+ // Sanitize message: escape special chars to prevent layout breaks
574
+ const escapedMessage = parts.message
575
+ .replace(/\r\n/g, "\\n")
576
+ .replace(/\n/g, "\\n")
577
+ .replace(/\r/g, "\\r")
578
+ .replace(/\t/g, "\\t");
579
+ // Limit message length to prevent Yoga layout engine errors
580
+ const MAX_MESSAGE_LENGTH = 1000;
581
+ const fullMessage = escapedMessage.length > MAX_MESSAGE_LENGTH
582
+ ? escapedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
583
+ : escapedMessage;
584
+ const cmd = parts.cmd
585
+ ? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
586
+ : "";
587
+ const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
588
+ // Map color names to theme colors
589
+ const levelColorMap = {
590
+ red: colors.error,
591
+ yellow: colors.warning,
592
+ blue: colors.primary,
593
+ gray: colors.textDim,
594
+ };
595
+ const sourceColorMap = {
596
+ magenta: "#d33682",
597
+ cyan: colors.info,
598
+ green: colors.success,
599
+ yellow: colors.warning,
600
+ gray: colors.textDim,
601
+ white: colors.text,
602
+ };
603
+ const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
604
+ const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
605
+ if (logsWrapMode) {
606
+ return (_jsxs(Box, { 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));
607
+ }
608
+ else {
609
+ // Calculate available width for message truncation
610
+ const timestampLen = parts.timestamp.length;
611
+ const levelLen = parts.level.length;
612
+ const sourceLen = parts.source.length + 2; // brackets
613
+ const shellLen = parts.shellName
614
+ ? parts.shellName.length + 3
615
+ : 0;
616
+ const cmdLen = cmd.length;
617
+ const exitLen = exitCode.length;
618
+ const spacesLen = 5; // spaces between elements
619
+ const metadataWidth = timestampLen +
620
+ levelLen +
621
+ sourceLen +
622
+ shellLen +
623
+ cmdLen +
624
+ exitLen +
625
+ spacesLen;
626
+ const safeTerminalWidth = Math.max(80, terminalWidth);
627
+ const availableMessageWidth = Math.max(20, safeTerminalWidth - metadataWidth);
628
+ const truncatedMessage = fullMessage.length > availableMessageWidth
629
+ ? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) + "..."
630
+ : fullMessage;
631
+ return (_jsxs(Box, { 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: truncatedMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
632
+ }
633
+ }) }), _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 logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, 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", " "] }), _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(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
567
634
  }
568
635
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
569
636
  }
@@ -598,7 +665,12 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
598
665
  snapshot: "Snapshot name (optional):",
599
666
  tunnel: "Port number to expose:",
600
667
  };
601
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === "exec"
668
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: (() => {
669
+ const name = devbox.name || devbox.id;
670
+ return name.length > 100
671
+ ? name.substring(0, 100) + "..."
672
+ : name;
673
+ })() }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === "exec"
602
674
  ? "ls -la"
603
675
  : executingOperation === "upload"
604
676
  ? "/path/to/file"
@@ -608,7 +680,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
608
680
  }
609
681
  // Operations selection mode - only show if not skipping
610
682
  if (!skipOperationsMenu || !executingOperation) {
611
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _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) => {
683
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _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) => {
612
684
  const isSelected = index === selectedOperation;
613
685
  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));
614
686
  }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, 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 } from "ink";
4
4
  import TextInput from "ink-text-input";
@@ -10,6 +10,7 @@ import { SuccessMessage } from "./SuccessMessage.js";
10
10
  import { Breadcrumb } from "./Breadcrumb.js";
11
11
  import { MetadataDisplay } from "./MetadataDisplay.js";
12
12
  import { colors } from "../utils/theme.js";
13
+ import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
13
14
  export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
14
15
  const [currentField, setCurrentField] = React.useState("create");
15
16
  const [formData, setFormData] = React.useState({
@@ -68,6 +69,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
68
69
  "CUSTOM_SIZE",
69
70
  ];
70
71
  const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
72
+ // Handle Ctrl+C to exit
73
+ useExitOnCtrlC();
71
74
  useInput((input, key) => {
72
75
  // Handle result screen
73
76
  if (result) {
@@ -97,7 +100,6 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
97
100
  }
98
101
  // Back to list
99
102
  if (input === "q" || key.escape) {
100
- console.clear();
101
103
  onBack();
102
104
  return;
103
105
  }
@@ -241,10 +243,13 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
241
243
  });
242
244
  }
243
245
  else if (currentField === "resource_size") {
244
- const currentIndex = resourceSizes.indexOf(formData.resource_size);
246
+ // Find current index, defaulting to 0 if not found (e.g., empty string)
247
+ const currentSize = formData.resource_size || "SMALL";
248
+ const currentIndex = resourceSizes.indexOf(currentSize);
249
+ const safeIndex = currentIndex === -1 ? 0 : currentIndex;
245
250
  const newIndex = key.leftArrow
246
- ? Math.max(0, currentIndex - 1)
247
- : Math.min(resourceSizes.length - 1, currentIndex + 1);
251
+ ? Math.max(0, safeIndex - 1)
252
+ : Math.min(resourceSizes.length - 1, safeIndex + 1);
248
253
  setFormData({
249
254
  ...formData,
250
255
  resource_size: resourceSizes[newIndex],
@@ -339,7 +344,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
339
344
  };
340
345
  // Result screen
341
346
  if (result) {
342
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || "(none)"}\nStatus: ${result.status}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
347
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SuccessMessage, { message: "Devbox created successfully!" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name || "(none)"] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Status: ", result.status] }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
343
348
  }
344
349
  // Error screen
345
350
  if (error) {
@@ -350,7 +355,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
350
355
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
351
356
  }
352
357
  // Form screen
353
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field, index) => {
358
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
354
359
  const isActive = currentField === field.key;
355
360
  const fieldData = formData[field.key];
356
361
  if (field.type === "action") {