@runloop/rl-cli 1.1.0 → 1.3.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 (55) hide show
  1. package/README.md +29 -8
  2. package/dist/commands/blueprint/list.js +97 -28
  3. package/dist/commands/blueprint/prune.js +258 -0
  4. package/dist/commands/devbox/create.js +3 -0
  5. package/dist/commands/devbox/list.js +44 -65
  6. package/dist/commands/menu.js +2 -1
  7. package/dist/commands/network-policy/create.js +27 -0
  8. package/dist/commands/network-policy/delete.js +21 -0
  9. package/dist/commands/network-policy/get.js +15 -0
  10. package/dist/commands/network-policy/list.js +494 -0
  11. package/dist/commands/object/list.js +516 -24
  12. package/dist/commands/snapshot/list.js +90 -29
  13. package/dist/components/Banner.js +109 -8
  14. package/dist/components/ConfirmationPrompt.js +45 -0
  15. package/dist/components/DevboxActionsMenu.js +42 -6
  16. package/dist/components/DevboxCard.js +1 -1
  17. package/dist/components/DevboxCreatePage.js +95 -81
  18. package/dist/components/DevboxDetailPage.js +218 -272
  19. package/dist/components/LogsViewer.js +8 -1
  20. package/dist/components/MainMenu.js +35 -4
  21. package/dist/components/NavigationTips.js +24 -0
  22. package/dist/components/NetworkPolicyCreatePage.js +264 -0
  23. package/dist/components/OperationsMenu.js +9 -1
  24. package/dist/components/ResourceActionsMenu.js +5 -1
  25. package/dist/components/ResourceDetailPage.js +204 -0
  26. package/dist/components/ResourceListView.js +19 -2
  27. package/dist/components/StatusBadge.js +2 -2
  28. package/dist/components/Table.js +6 -8
  29. package/dist/components/form/FormActionButton.js +7 -0
  30. package/dist/components/form/FormField.js +7 -0
  31. package/dist/components/form/FormListManager.js +112 -0
  32. package/dist/components/form/FormSelect.js +34 -0
  33. package/dist/components/form/FormTextInput.js +8 -0
  34. package/dist/components/form/index.js +8 -0
  35. package/dist/hooks/useViewportHeight.js +38 -20
  36. package/dist/router/Router.js +23 -1
  37. package/dist/screens/BlueprintDetailScreen.js +337 -0
  38. package/dist/screens/MenuScreen.js +6 -0
  39. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  40. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  41. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  42. package/dist/screens/ObjectDetailScreen.js +377 -0
  43. package/dist/screens/ObjectListScreen.js +7 -0
  44. package/dist/screens/SnapshotDetailScreen.js +208 -0
  45. package/dist/services/blueprintService.js +30 -11
  46. package/dist/services/networkPolicyService.js +108 -0
  47. package/dist/services/objectService.js +101 -0
  48. package/dist/services/snapshotService.js +39 -3
  49. package/dist/store/blueprintStore.js +4 -10
  50. package/dist/store/index.js +1 -0
  51. package/dist/store/networkPolicyStore.js +83 -0
  52. package/dist/store/objectStore.js +92 -0
  53. package/dist/store/snapshotStore.js +4 -8
  54. package/dist/utils/commands.js +58 -0
  55. package/package.json +2 -2
@@ -1,16 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * DevboxDetailPage - Detail page for devboxes
4
+ * Uses the generic ResourceDetailPage component with devbox-specific customizations
5
+ */
2
6
  import React from "react";
3
- import { Box, Text, useInput } from "ink";
7
+ import { Text } from "ink";
4
8
  import figures from "figures";
5
- import { Header } from "./Header.js";
6
- import { StatusBadge } from "./StatusBadge.js";
7
- import { Breadcrumb } from "./Breadcrumb.js";
8
9
  import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
9
10
  import { StateHistory } from "./StateHistory.js";
11
+ import { ResourceDetailPage, } from "./ResourceDetailPage.js";
10
12
  import { getDevboxUrl } from "../utils/url.js";
11
13
  import { colors } from "../utils/theme.js";
12
- import { useViewportHeight } from "../hooks/useViewportHeight.js";
13
- import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
14
14
  import { getDevbox } from "../services/devboxService.js";
15
15
  // Format time ago in a succinct way
16
16
  const formatTimeAgo = (timestamp) => {
@@ -32,59 +32,11 @@ const formatTimeAgo = (timestamp) => {
32
32
  const years = Math.floor(months / 12);
33
33
  return `${years}y ago`;
34
34
  };
35
- // Truncate long strings to prevent layout issues
36
- const truncateString = (str, maxLength) => {
37
- if (str.length <= maxLength)
38
- return str;
39
- return str.substring(0, maxLength - 3) + "...";
40
- };
41
35
  export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
42
- const isMounted = React.useRef(true);
43
- // Track mounted state
44
- React.useEffect(() => {
45
- isMounted.current = true;
46
- return () => {
47
- isMounted.current = false;
48
- };
49
- }, []);
50
- // Local state for devbox data (updated by polling)
51
- const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
52
- const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
53
- const [detailScroll, setDetailScroll] = React.useState(0);
54
36
  const [showActions, setShowActions] = React.useState(false);
55
- const [selectedOperation, setSelectedOperation] = React.useState(0);
56
- // Background polling for devbox details
57
- React.useEffect(() => {
58
- // Skip polling if showing actions, detailed info, or not mounted
59
- if (showActions || showDetailedInfo)
60
- return;
61
- const interval = setInterval(async () => {
62
- // Only poll when not in actions/detail mode and component is mounted
63
- if (!showActions && !showDetailedInfo && isMounted.current) {
64
- try {
65
- const updatedDevbox = await getDevbox(initialDevbox.id);
66
- // Only update if still mounted
67
- if (isMounted.current) {
68
- setCurrentDevbox(updatedDevbox);
69
- }
70
- }
71
- catch {
72
- // Silently ignore polling errors to avoid disrupting user experience
73
- }
74
- }
75
- }, 3000); // Poll every 3 seconds
76
- return () => clearInterval(interval);
77
- }, [initialDevbox.id, showActions, showDetailedInfo]);
78
- // Calculate viewport for detailed info view:
79
- // - Breadcrumb (3 lines + marginBottom): 4 lines
80
- // - Header (title + underline + marginBottom): 3 lines
81
- // - Status box (content + marginBottom): 2 lines
82
- // - Content box (marginTop + border + paddingY top/bottom + border + marginBottom): 6 lines
83
- // - Help bar (marginTop + content): 2 lines
84
- // - Safety buffer: 1 line
85
- // Total: 18 lines
86
- const detailViewport = useViewportHeight({ overhead: 18, minHeight: 10 });
87
- const selectedDevbox = currentDevbox;
37
+ const [selectedOperationKey, setSelectedOperationKey] = React.useState(null);
38
+ const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
39
+ // All possible operations for devboxes
88
40
  const allOperations = [
89
41
  {
90
42
  key: "logs",
@@ -151,9 +103,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
151
103
  },
152
104
  ];
153
105
  // Filter operations based on devbox status
154
- const operations = selectedDevbox
155
- ? allOperations.filter((op) => {
156
- const status = selectedDevbox.status;
106
+ const getFilteredOperations = (devbox) => {
107
+ return allOperations.filter((op) => {
108
+ const status = devbox.status;
157
109
  // When suspended: logs and resume
158
110
  if (status === "suspended") {
159
111
  return op.key === "resume" || op.key === "logs";
@@ -170,125 +122,190 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
170
122
  }
171
123
  // Default for transitional states (provisioning, initializing)
172
124
  return op.key === "logs" || op.key === "delete";
173
- })
174
- : allOperations;
175
- const formattedCreateTime = selectedDevbox.create_time_ms
176
- ? new Date(selectedDevbox.create_time_ms).toLocaleString()
177
- : "";
178
- const createTimeAgo = selectedDevbox.create_time_ms
179
- ? formatTimeAgo(selectedDevbox.create_time_ms)
180
- : "";
181
- // Handle Ctrl+C to exit
182
- useExitOnCtrlC();
183
- useInput((input, key) => {
184
- // Don't process input if unmounting
185
- if (!isMounted.current)
186
- return;
187
- // Skip input handling when in actions view
188
- if (showActions) {
189
- return;
190
- }
191
- // Handle detailed info mode
192
- if (showDetailedInfo) {
193
- if (input === "q" || key.escape) {
194
- setShowDetailedInfo(false);
195
- setDetailScroll(0);
196
- }
197
- else if (input === "j" || input === "s" || key.downArrow) {
198
- // Scroll down in detailed info
199
- setDetailScroll(detailScroll + 1);
200
- }
201
- else if (input === "k" || input === "w" || key.upArrow) {
202
- // Scroll up in detailed info
203
- setDetailScroll(Math.max(0, detailScroll - 1));
125
+ });
126
+ };
127
+ // Build detail sections for the devbox
128
+ const buildDetailSections = (devbox) => {
129
+ const sections = [];
130
+ const lp = devbox.launch_parameters;
131
+ // Calculate uptime
132
+ const uptime = devbox.create_time_ms
133
+ ? Math.floor((Date.now() - devbox.create_time_ms) / 1000 / 60)
134
+ : null;
135
+ // Details section
136
+ const detailFields = [];
137
+ // Created / Ended time
138
+ if (devbox.create_time_ms) {
139
+ const createTime = new Date(devbox.create_time_ms).toLocaleString();
140
+ const timeAgo = formatTimeAgo(devbox.create_time_ms);
141
+ if (devbox.end_time_ms) {
142
+ const endTime = new Date(devbox.end_time_ms).toLocaleString();
143
+ detailFields.push({
144
+ label: "Created",
145
+ value: `${createTime} ${endTime}`,
146
+ });
204
147
  }
205
- else if (key.pageDown) {
206
- // Page down
207
- setDetailScroll(detailScroll + 10);
148
+ else {
149
+ detailFields.push({
150
+ label: "Created",
151
+ value: `${createTime} (${timeAgo})`,
152
+ });
208
153
  }
209
- else if (key.pageUp) {
210
- // Page up
211
- setDetailScroll(Math.max(0, detailScroll - 10));
154
+ }
155
+ // Resources
156
+ if (lp?.resource_size_request ||
157
+ lp?.custom_cpu_cores ||
158
+ lp?.custom_gb_memory ||
159
+ lp?.custom_disk_size ||
160
+ lp?.architecture) {
161
+ const resources = [
162
+ lp?.resource_size_request,
163
+ lp?.architecture,
164
+ lp?.custom_cpu_cores && `${lp.custom_cpu_cores}VCPU`,
165
+ lp?.custom_gb_memory && `${lp.custom_gb_memory}GB RAM`,
166
+ lp?.custom_disk_size && `${lp.custom_disk_size}GB DISC`,
167
+ ]
168
+ .filter(Boolean)
169
+ .join(" • ");
170
+ detailFields.push({
171
+ label: "Resources",
172
+ value: resources,
173
+ });
174
+ }
175
+ // Lifetime with remaining time
176
+ if (lp?.keep_alive_time_seconds) {
177
+ const lifetimeStr = lp.keep_alive_time_seconds < 3600
178
+ ? `${Math.floor(lp.keep_alive_time_seconds / 60)}m`
179
+ : `${Math.floor(lp.keep_alive_time_seconds / 3600)}h ${Math.floor((lp.keep_alive_time_seconds % 3600) / 60)}m`;
180
+ let remainingText = "";
181
+ if (uptime !== null && devbox.status === "running") {
182
+ const maxLifetimeMinutes = Math.floor(lp.keep_alive_time_seconds / 60);
183
+ const remainingMinutes = maxLifetimeMinutes - uptime;
184
+ if (remainingMinutes <= 0) {
185
+ remainingText = " (Expired)";
186
+ }
187
+ else if (remainingMinutes < 60) {
188
+ remainingText = ` (${remainingMinutes}m remaining)`;
189
+ }
190
+ else {
191
+ const hours = Math.floor(remainingMinutes / 60);
192
+ const mins = remainingMinutes % 60;
193
+ remainingText = ` (${hours}h ${mins}m remaining)`;
194
+ }
212
195
  }
213
- return;
196
+ detailFields.push({
197
+ label: "Lifetime",
198
+ value: lifetimeStr + remainingText,
199
+ });
214
200
  }
215
- // Main view input handling
216
- if (input === "q" || key.escape) {
217
- onBack();
201
+ // User
202
+ if (lp?.user_parameters) {
203
+ const username = lp.user_parameters.username || "default";
204
+ const uid = lp.user_parameters.uid != null && lp.user_parameters.uid !== 0
205
+ ? ` (UID: ${lp.user_parameters.uid})`
206
+ : "";
207
+ detailFields.push({
208
+ label: "User",
209
+ value: username + uid,
210
+ });
218
211
  }
219
- else if (input === "i") {
220
- setShowDetailedInfo(true);
221
- setDetailScroll(0);
212
+ // Source
213
+ if (devbox.blueprint_id || devbox.snapshot_id) {
214
+ detailFields.push({
215
+ label: "Source",
216
+ value: (_jsx(Text, { color: colors.success, children: devbox.blueprint_id || devbox.snapshot_id })),
217
+ });
222
218
  }
223
- else if (key.upArrow && selectedOperation > 0) {
224
- setSelectedOperation(selectedOperation - 1);
219
+ // Network Policy
220
+ if (lp?.network_policy_id) {
221
+ detailFields.push({
222
+ label: "Network Policy",
223
+ value: _jsx(Text, { color: colors.info, children: lp.network_policy_id }),
224
+ });
225
225
  }
226
- else if (key.downArrow && selectedOperation < operations.length - 1) {
227
- setSelectedOperation(selectedOperation + 1);
226
+ // Initiator
227
+ if (devbox.initiator_id) {
228
+ detailFields.push({
229
+ label: "Initiator",
230
+ value: _jsx(Text, { color: colors.secondary, children: devbox.initiator_id }),
231
+ });
228
232
  }
229
- else if (key.return || input === "a") {
230
- setShowActions(true);
233
+ // Capabilities
234
+ const hasCapabilities = devbox.capabilities &&
235
+ devbox.capabilities.filter((c) => c !== "unknown").length > 0;
236
+ if (hasCapabilities) {
237
+ detailFields.push({
238
+ label: "Capabilities",
239
+ value: devbox.capabilities
240
+ .filter((c) => c !== "unknown")
241
+ .join(", "),
242
+ });
231
243
  }
232
- else if (input) {
233
- // Check if input matches any operation shortcut
234
- const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
235
- if (matchedOpIndex !== -1) {
236
- setSelectedOperation(matchedOpIndex);
237
- setShowActions(true);
238
- }
244
+ if (detailFields.length > 0) {
245
+ sections.push({
246
+ title: "Details",
247
+ icon: figures.squareSmallFilled,
248
+ color: colors.warning,
249
+ fields: detailFields,
250
+ });
239
251
  }
240
- if (input === "o") {
241
- // Open in browser
242
- const url = getDevboxUrl(selectedDevbox.id);
243
- const openBrowser = async () => {
244
- const { exec } = await import("child_process");
245
- const platform = process.platform;
246
- let openCommand;
247
- if (platform === "darwin") {
248
- openCommand = `open "${url}"`;
249
- }
250
- else if (platform === "win32") {
251
- openCommand = `start "${url}"`;
252
- }
253
- else {
254
- openCommand = `xdg-open "${url}"`;
255
- }
256
- exec(openCommand);
257
- };
258
- openBrowser();
252
+ // Metadata section
253
+ if (devbox.metadata && Object.keys(devbox.metadata).length > 0) {
254
+ sections.push({
255
+ title: "Metadata",
256
+ icon: figures.identical,
257
+ color: colors.secondary,
258
+ fields: Object.entries(devbox.metadata).map(([key, value]) => ({
259
+ label: key,
260
+ value: value,
261
+ })),
262
+ });
259
263
  }
260
- });
261
- const uptime = selectedDevbox.create_time_ms
262
- ? Math.floor((Date.now() - selectedDevbox.create_time_ms) / 1000 / 60)
263
- : null;
264
- // Build detailed info lines for scrolling
265
- const buildDetailLines = () => {
264
+ // Error section
265
+ if (devbox.failure_reason) {
266
+ sections.push({
267
+ title: "Error",
268
+ icon: figures.cross,
269
+ color: colors.error,
270
+ fields: [
271
+ {
272
+ label: "Failure Reason",
273
+ value: devbox.failure_reason,
274
+ color: colors.error,
275
+ },
276
+ ],
277
+ });
278
+ }
279
+ return sections;
280
+ };
281
+ // Build detailed info lines for full details view
282
+ const buildDetailLines = (devbox) => {
266
283
  const lines = [];
267
284
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
268
285
  // Core Information
269
286
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Devbox Details" }, "core-title"));
270
- lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.id] }, "core-id"));
271
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", selectedDevbox.name || "(none)"] }, "core-name"));
272
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(selectedDevbox.status)] }, "core-status"));
273
- if (selectedDevbox.create_time_ms) {
274
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(selectedDevbox.create_time_ms).toLocaleString()] }, "core-created"));
287
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", devbox.id] }, "core-id"));
288
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", devbox.name || "(none)"] }, "core-name"));
289
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(devbox.status)] }, "core-status"));
290
+ if (devbox.create_time_ms) {
291
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(devbox.create_time_ms).toLocaleString()] }, "core-created"));
275
292
  }
276
- if (selectedDevbox.end_time_ms) {
277
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(selectedDevbox.end_time_ms).toLocaleString()] }, "core-ended"));
293
+ if (devbox.end_time_ms) {
294
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(devbox.end_time_ms).toLocaleString()] }, "core-ended"));
278
295
  }
279
296
  lines.push(_jsx(Text, { children: " " }, "core-space"));
280
297
  // Capabilities
281
- if (selectedDevbox.capabilities && selectedDevbox.capabilities.length > 0) {
298
+ if (devbox.capabilities && devbox.capabilities.length > 0) {
282
299
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Capabilities" }, "cap-title"));
283
- selectedDevbox.capabilities.forEach((cap, idx) => {
300
+ devbox.capabilities.forEach((cap, idx) => {
284
301
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cap] }, `cap-${idx}`));
285
302
  });
286
303
  lines.push(_jsx(Text, { children: " " }, "cap-space"));
287
304
  }
288
305
  // Launch Parameters
289
- if (selectedDevbox.launch_parameters) {
306
+ if (devbox.launch_parameters) {
290
307
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Launch Parameters" }, "launch-title"));
291
- const lp = selectedDevbox.launch_parameters;
308
+ const lp = devbox.launch_parameters;
292
309
  if (lp.resource_size_request) {
293
310
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Resource Size Request: ", lp.resource_size_request] }, "launch-size-req"));
294
311
  }
@@ -315,14 +332,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
315
332
  }
316
333
  if (lp.launch_commands && lp.launch_commands.length > 0) {
317
334
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "launch-launch-cmds"));
318
- // lp.launch_commands.forEach((cmd: string, idx: number) => {
319
- // lines.push(
320
- // <Text key={`launch-cmd-${idx}`} dimColor>
321
- // {" "}
322
- // {figures.pointer} {cmd}
323
- // </Text>,
324
- // );
325
- // });
335
+ lp.launch_commands.forEach((cmd, idx) => {
336
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `launch-cmd-${idx}`));
337
+ });
326
338
  }
327
339
  if (lp.required_services && lp.required_services.length > 0) {
328
340
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Required Services: ", lp.required_services.join(", ")] }, "launch-services"));
@@ -330,59 +342,57 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
330
342
  if (lp.user_parameters) {
331
343
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "User Parameters:"] }, "launch-user"));
332
344
  if (lp.user_parameters.username) {
333
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Username: ", lp.user_parameters.username] }, "user-name"));
345
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Username: ", lp.user_parameters.username] }, "user-name"));
334
346
  }
335
347
  if (lp.user_parameters.uid) {
336
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "UID: ", lp.user_parameters.uid] }, "user-uid"));
348
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "UID: ", lp.user_parameters.uid] }, "user-uid"));
337
349
  }
338
350
  }
339
351
  lines.push(_jsx(Text, { children: " " }, "launch-space"));
340
352
  }
341
353
  // Source
342
- if (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) {
354
+ if (devbox.blueprint_id || devbox.snapshot_id) {
343
355
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
344
- if (selectedDevbox.blueprint_id) {
345
- lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.blueprint_id] }, "source-bp"));
356
+ if (devbox.blueprint_id) {
357
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Blueprint: ", devbox.blueprint_id] }, "source-bp"));
346
358
  }
347
- if (selectedDevbox.snapshot_id) {
348
- lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.snapshot_id] }, "source-snap"));
359
+ if (devbox.snapshot_id) {
360
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Snapshot: ", devbox.snapshot_id] }, "source-snap"));
349
361
  }
350
362
  lines.push(_jsx(Text, { children: " " }, "source-space"));
351
363
  }
352
364
  // Initiator
353
- if (selectedDevbox.initiator_type) {
365
+ if (devbox.initiator_type) {
354
366
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Initiator" }, "init-title"));
355
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", selectedDevbox.initiator_type] }, "init-type"));
356
- if (selectedDevbox.initiator_id) {
357
- lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.initiator_id] }, "init-id"));
367
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", devbox.initiator_type] }, "init-type"));
368
+ if (devbox.initiator_id) {
369
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", devbox.initiator_id] }, "init-id"));
358
370
  }
359
371
  lines.push(_jsx(Text, { children: " " }, "init-space"));
360
372
  }
361
373
  // Status Details
362
- if (selectedDevbox.failure_reason || selectedDevbox.shutdown_reason) {
374
+ if (devbox.failure_reason || devbox.shutdown_reason) {
363
375
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Status Details" }, "status-title"));
364
- if (selectedDevbox.failure_reason) {
365
- lines.push(_jsxs(Text, { color: colors.error, dimColor: true, children: [" ", "Failure Reason: ", selectedDevbox.failure_reason] }, "status-fail"));
376
+ if (devbox.failure_reason) {
377
+ lines.push(_jsxs(Text, { color: colors.error, children: [" ", "Failure Reason: ", devbox.failure_reason] }, "status-fail"));
366
378
  }
367
- if (selectedDevbox.shutdown_reason) {
368
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", selectedDevbox.shutdown_reason] }, "status-shut"));
379
+ if (devbox.shutdown_reason) {
380
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", devbox.shutdown_reason] }, "status-shut"));
369
381
  }
370
382
  lines.push(_jsx(Text, { children: " " }, "status-space"));
371
383
  }
372
384
  // Metadata
373
- if (selectedDevbox.metadata &&
374
- Object.keys(selectedDevbox.metadata).length > 0) {
385
+ if (devbox.metadata && Object.keys(devbox.metadata).length > 0) {
375
386
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
376
- Object.entries(selectedDevbox.metadata).forEach(([key, value], idx) => {
387
+ Object.entries(devbox.metadata).forEach(([key, value], idx) => {
377
388
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
378
389
  });
379
390
  lines.push(_jsx(Text, { children: " " }, "meta-space"));
380
391
  }
381
392
  // State Transitions
382
- if (selectedDevbox.state_transitions &&
383
- selectedDevbox.state_transitions.length > 0) {
393
+ if (devbox.state_transitions && devbox.state_transitions.length > 0) {
384
394
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "State History" }, "state-title"));
385
- selectedDevbox.state_transitions.forEach((transition, idx) => {
395
+ devbox.state_transitions.forEach((transition, idx) => {
386
396
  const text = `${idx + 1}. ${capitalize(transition.status)}${transition.transition_time_ms ? ` at ${new Date(transition.transition_time_ms).toLocaleString()}` : ""}`;
387
397
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", text] }, `state-${idx}`));
388
398
  });
@@ -390,102 +400,38 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
390
400
  }
391
401
  // Raw JSON (full)
392
402
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
393
- const jsonLines = JSON.stringify(selectedDevbox, null, 2).split("\n");
403
+ const jsonLines = JSON.stringify(devbox, null, 2).split("\n");
394
404
  jsonLines.forEach((line, idx) => {
395
405
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
396
406
  });
397
407
  return lines;
398
408
  };
399
- // Actions view - show the DevboxActionsMenu when an action is triggered
409
+ // Handle operation selection
410
+ const handleOperation = (operation, _devbox) => {
411
+ setSelectedOperationKey(operation);
412
+ setShowActions(true);
413
+ };
414
+ // Polling function
415
+ const pollDevbox = React.useCallback(async () => {
416
+ const updated = await getDevbox(initialDevbox.id);
417
+ setCurrentDevbox(updated);
418
+ return updated;
419
+ }, [initialDevbox.id]);
420
+ // Show DevboxActionsMenu when an action is selected
400
421
  if (showActions) {
401
- const selectedOp = operations[selectedOperation];
402
- return (_jsx(DevboxActionsMenu, { devbox: selectedDevbox, onBack: () => {
422
+ return (_jsx(DevboxActionsMenu, { devbox: currentDevbox, onBack: () => {
403
423
  setShowActions(false);
404
- setSelectedOperation(0);
424
+ setSelectedOperationKey(null);
405
425
  }, breadcrumbItems: [
406
426
  { label: "Devboxes" },
407
- { label: selectedDevbox.name || selectedDevbox.id },
408
- ], initialOperation: selectedOp?.key, skipOperationsMenu: true }));
409
- }
410
- // Detailed info mode - full screen
411
- if (showDetailedInfo) {
412
- const detailLines = buildDetailLines();
413
- const viewportHeight = detailViewport.viewportHeight;
414
- const maxScroll = Math.max(0, detailLines.length - viewportHeight);
415
- const actualScroll = Math.min(detailScroll, maxScroll);
416
- const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
417
- const hasMore = actualScroll + viewportHeight < detailLines.length;
418
- const hasLess = actualScroll > 0;
419
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
420
- { label: "Devboxes" },
421
- { label: selectedDevbox.name || selectedDevbox.id },
422
- { label: "Full Details", active: true },
423
- ] }), _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"] })] })] }));
427
+ { label: currentDevbox.name || currentDevbox.id },
428
+ ], initialOperation: selectedOperationKey || undefined, skipOperationsMenu: true }));
424
429
  }
425
- // Main detail view
426
- const lp = selectedDevbox.launch_parameters;
427
- const hasCapabilities = selectedDevbox.capabilities &&
428
- selectedDevbox.capabilities.filter((c) => c !== "unknown").length >
429
- 0;
430
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
431
- { label: "Devboxes" },
432
- { label: selectedDevbox.name || selectedDevbox.id, active: true },
433
- ] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [_jsx(Text, { color: colors.primary, bold: true, children: truncateString(selectedDevbox.name || selectedDevbox.id, Math.max(20, detailViewport.terminalWidth - 35)) }), selectedDevbox.name && (_jsxs(Text, { color: colors.idColor, children: [" \u2022 ", selectedDevbox.id] }))] }), _jsxs(Box, { children: [_jsx(StatusBadge, { status: selectedDevbox.status, fullText: true }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { color: colors.success, dimColor: true, children: [" ", "\u2022 Uptime:", " ", uptime < 60
434
- ? `${uptime}m`
435
- : `${Math.floor(uptime / 60)}h ${uptime % 60}m`] })), selectedDevbox.status !== "running" &&
436
- selectedDevbox.create_time_ms &&
437
- selectedDevbox.end_time_ms && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Ran for:", " ", (() => {
438
- const runtime = Math.floor((selectedDevbox.end_time_ms -
439
- selectedDevbox.create_time_ms) /
440
- 1000);
441
- if (runtime < 60)
442
- return `${runtime}s`;
443
- const mins = Math.floor(runtime / 60);
444
- if (mins < 60)
445
- return `${mins}m ${runtime % 60}s`;
446
- const hours = Math.floor(mins / 60);
447
- return `${hours}h ${mins % 60}m`;
448
- })()] }))] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Details"] }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [selectedDevbox.create_time_ms && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Created " }), _jsx(Text, { dimColor: true, children: formattedCreateTime }), selectedDevbox.end_time_ms ? (_jsxs(Text, { dimColor: true, children: [" ", figures.arrowRight, " ", new Date(selectedDevbox.end_time_ms).toLocaleString()] })) : (_jsxs(Text, { dimColor: true, children: [" (", createTimeAgo, ")"] }))] })), (lp?.resource_size_request ||
449
- lp?.custom_cpu_cores ||
450
- lp?.custom_gb_memory ||
451
- lp?.custom_disk_size ||
452
- lp?.architecture) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Resources " }), _jsx(Text, { dimColor: true, children: [
453
- lp?.resource_size_request,
454
- lp?.architecture,
455
- lp?.custom_cpu_cores && `${lp.custom_cpu_cores}VCPU`,
456
- lp?.custom_gb_memory && `${lp.custom_gb_memory}GB RAM`,
457
- lp?.custom_disk_size && `${lp.custom_disk_size}GB DISC`,
458
- ]
459
- .filter(Boolean)
460
- .join(" • ") })] })), (lp?.keep_alive_time_seconds || lp?.user_parameters) && (_jsxs(Box, { children: [lp?.keep_alive_time_seconds && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: "Lifetime " }), _jsx(Text, { dimColor: true, children: lp.keep_alive_time_seconds < 3600
461
- ? `${Math.floor(lp.keep_alive_time_seconds / 60)}m`
462
- : `${Math.floor(lp.keep_alive_time_seconds / 3600)}h ${Math.floor((lp.keep_alive_time_seconds % 3600) / 60)}m` }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { children: [" ", "\u2022", " ", (() => {
463
- const maxLifetimeMinutes = Math.floor(lp.keep_alive_time_seconds / 60);
464
- const remainingMinutes = maxLifetimeMinutes - uptime;
465
- if (remainingMinutes <= 0) {
466
- return _jsx(Text, { color: colors.error, children: "Expired" });
467
- }
468
- else if (remainingMinutes < 5) {
469
- return (_jsxs(Text, { color: colors.error, children: [remainingMinutes, "m remaining"] }));
470
- }
471
- else if (remainingMinutes < 15) {
472
- return (_jsxs(Text, { color: colors.warning, children: [remainingMinutes, "m remaining"] }));
473
- }
474
- else if (remainingMinutes < 60) {
475
- return (_jsxs(Text, { color: colors.success, children: [remainingMinutes, "m remaining"] }));
476
- }
477
- else {
478
- const hours = Math.floor(remainingMinutes / 60);
479
- const mins = remainingMinutes % 60;
480
- return (_jsxs(Text, { color: colors.success, children: [hours, "h ", mins, "m remaining"] }));
481
- }
482
- })()] })), lp?.user_parameters && (_jsx(Text, { color: colors.textDim, children: " \u2022 " }))] })), lp?.user_parameters && (_jsxs(_Fragment, { children: [!lp?.keep_alive_time_seconds && (_jsx(Text, { color: colors.textDim, children: "User " })), _jsx(Text, { color: colors.textDim, children: "User: " }), _jsxs(Text, { dimColor: true, children: [lp.user_parameters.username || "default", lp.user_parameters.uid != null &&
483
- lp.user_parameters.uid !== 0 &&
484
- ` (UID: ${lp.user_parameters.uid})`] })] }))] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Source " }), _jsx(Text, { color: colors.success, children: selectedDevbox.blueprint_id || selectedDevbox.snapshot_id })] })), selectedDevbox.initiator_id && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Initiator " }), _jsx(Text, { color: colors.secondary, children: selectedDevbox.initiator_id })] })), hasCapabilities && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Capabilities " }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
485
- .filter((c) => c !== "unknown")
486
- .join(", ") })] }))] })] }), selectedDevbox.metadata &&
487
- Object.keys(selectedDevbox.metadata).length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.identical, " Metadata"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: Object.entries(selectedDevbox.metadata).map(([key, value]) => (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: key }), _jsx(Text, { color: colors.textDim, children: ": " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: value })] }, key))) })] })), selectedDevbox.failure_reason && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Error"] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: colors.error, children: selectedDevbox.failure_reason }) })] })), _jsx(StateHistory, { stateTransitions: selectedDevbox.state_transitions, shutdownReason: selectedDevbox.shutdown_reason ?? undefined }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: operations.map((op, index) => {
488
- const isSelected = index === selectedOperation;
489
- 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));
490
- }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
430
+ // Determine if we should poll based on status
431
+ const shouldPoll = currentDevbox.status === "running" ||
432
+ currentDevbox.status === "provisioning" ||
433
+ currentDevbox.status === "initializing" ||
434
+ currentDevbox.status === "resuming" ||
435
+ currentDevbox.status === "suspending";
436
+ return (_jsx(ResourceDetailPage, { resource: currentDevbox, resourceType: "Devboxes", getDisplayName: (d) => d.name || d.id, getId: (d) => d.id, getStatus: (d) => d.status, getUrl: (d) => getDevboxUrl(d.id), detailSections: buildDetailSections(currentDevbox), operations: getFilteredOperations(currentDevbox), onOperation: handleOperation, onBack: onBack, buildDetailLines: buildDetailLines, additionalContent: _jsx(StateHistory, { stateTransitions: currentDevbox.state_transitions, shutdownReason: currentDevbox.shutdown_reason ?? undefined }), pollResource: shouldPoll ? pollDevbox : undefined, pollInterval: 3000 }));
491
437
  };