@runloop/rl-cli 0.5.0 → 0.10.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.
@@ -4,9 +4,9 @@ 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";
7
- import { MetadataDisplay } from "./MetadataDisplay.js";
8
7
  import { Breadcrumb } from "./Breadcrumb.js";
9
8
  import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
9
+ import { StateHistory } from "./StateHistory.js";
10
10
  import { getDevboxUrl } from "../utils/url.js";
11
11
  import { colors } from "../utils/theme.js";
12
12
  import { useViewportHeight } from "../hooks/useViewportHeight.js";
@@ -32,6 +32,12 @@ 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
+ };
35
41
  export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
36
42
  const isMounted = React.useRef(true);
37
43
  // Track mounted state
@@ -359,7 +365,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
359
365
  lines.push(_jsxs(Text, { color: colors.error, dimColor: true, children: [" ", "Failure Reason: ", selectedDevbox.failure_reason] }, "status-fail"));
360
366
  }
361
367
  if (selectedDevbox.shutdown_reason) {
362
- lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Reason: ", selectedDevbox.shutdown_reason] }, "status-shut"));
368
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", selectedDevbox.shutdown_reason] }, "status-shut"));
363
369
  }
364
370
  lines.push(_jsx(Text, { children: " " }, "status-space"));
365
371
  }
@@ -424,16 +430,61 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
424
430
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
425
431
  { label: "Devboxes" },
426
432
  { label: selectedDevbox.name || selectedDevbox.id, active: true },
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
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
428
434
  ? `${uptime}m`
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 ||
430
- lp?.custom_cpu_cores ||
431
- lp?.custom_gb_memory ||
432
- lp?.custom_disk_size ||
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
434
- .filter((c) => c !== "unknown")
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 &&
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) => {
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) => {
437
488
  const isSelected = index === selectedOperation;
438
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));
439
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"] }) })] }));
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
2
3
  import { Box, Text } from "ink";
3
4
  import figures from "figures";
4
5
  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
+ 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 })] }));
7
+ const renderCompactKeyValue = (keyText, value, color, isLast) => (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: colors.textDim, children: "=" }), _jsx(Text, { color: color, children: value }), !isLast && _jsx(Text, { color: colors.textDim, children: " \u00B7 " })] }));
6
8
  // Generate color for each key based on hash
7
9
  const getColorForKey = (key, index) => {
8
10
  const colorList = [
@@ -15,11 +17,19 @@ const getColorForKey = (key, index) => {
15
17
  ];
16
18
  return colorList[index % colorList.length];
17
19
  };
18
- export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, }) => {
20
+ export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, compact = false, }) => {
19
21
  const entries = Object.entries(metadata);
20
22
  if (entries.length === 0) {
21
23
  return null;
22
24
  }
25
+ if (compact) {
26
+ return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] })), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: entries.map(([key, value], index) => {
27
+ const color = getColorForKey(key, index);
28
+ const isSelected = selectedKey === key;
29
+ const isLast = index === entries.length - 1;
30
+ return (_jsxs(React.Fragment, { children: [isSelected && (_jsx(Text, { color: colors.primary, bold: true, children: figures.pointer })), renderCompactKeyValue(key, value, isSelected ? colors.primary : color, isLast)] }, key));
31
+ }) })] }));
32
+ }
23
33
  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
34
  const color = getColorForKey(key, index);
25
35
  const isSelected = selectedKey === key;
@@ -10,25 +10,38 @@ import { Table } from "./Table.js";
10
10
  import { colors } from "../utils/theme.js";
11
11
  import { useViewportHeight } from "../hooks/useViewportHeight.js";
12
12
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
13
- // Format time ago in a succinct way
13
+ // Format time ago - concise with ISO-style date for older items
14
14
  export const formatTimeAgo = (timestamp) => {
15
15
  const seconds = Math.floor((Date.now() - timestamp) / 1000);
16
+ const date = new Date(timestamp);
17
+ const now = new Date();
18
+ // Format time as HH:MM:SS (24h)
19
+ const time = date.toTimeString().slice(0, 8);
20
+ // Less than 1 minute
16
21
  if (seconds < 60)
17
- return `${seconds}s ago`;
22
+ return `${time} (${seconds}s ago)`;
18
23
  const minutes = Math.floor(seconds / 60);
24
+ // Less than 1 hour
19
25
  if (minutes < 60)
20
- return `${minutes}m ago`;
26
+ return `${time} (${minutes}m ago)`;
21
27
  const hours = Math.floor(minutes / 60);
28
+ // Less than 24 hours - show time + relative
22
29
  if (hours < 24)
23
- return `${hours}h ago`;
30
+ return `${time} (${hours}hr ago)`;
24
31
  const days = Math.floor(hours / 24);
25
- if (days < 30)
26
- return `${days}d ago`;
27
- const months = Math.floor(days / 30);
28
- if (months < 12)
29
- return `${months}mo ago`;
30
- const years = Math.floor(months / 12);
31
- return `${years}y ago`;
32
+ const sameYear = date.getFullYear() === now.getFullYear();
33
+ // Format date parts
34
+ const month = String(date.getMonth() + 1).padStart(2, "0");
35
+ const day = String(date.getDate()).padStart(2, "0");
36
+ const year = date.getFullYear();
37
+ // Date format: MM-DD or YYYY-MM-DD if different year
38
+ const dateStr = `${month}-${day}`;
39
+ // 1-7 days - show date + time + relative
40
+ if (days <= 7) {
41
+ return `${dateStr} ${time} (${days}d)`;
42
+ }
43
+ // More than 7 days - just date + time, no relative
44
+ return `${dateStr} ${time}`;
32
45
  };
33
46
  export function ResourceListView({ config }) {
34
47
  const { exit: inkExit } = useApp();
@@ -0,0 +1,120 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import figures from "figures";
4
+ import { colors } from "../utils/theme.js";
5
+ import { getStatusDisplay } from "./StatusBadge.js";
6
+ // Format shutdown reason into human-readable text
7
+ const formatShutdownReason = (reason) => {
8
+ switch (reason) {
9
+ case "api_shutdown":
10
+ return "API call";
11
+ case "idle_timeout":
12
+ return "Idle timeout";
13
+ case "keep_alive_timeout":
14
+ return "Max lifetime expired";
15
+ case "max_lifetime":
16
+ case "max_lifetime_exceeded":
17
+ return "Max lifetime exceeded";
18
+ case "user_initiated":
19
+ return "User initiated";
20
+ case "system_maintenance":
21
+ return "System maintenance";
22
+ case "resource_limit":
23
+ return "Resource limits";
24
+ case "entrypoint_exit":
25
+ return "Entrypoint exited";
26
+ case "idle":
27
+ return "Idle";
28
+ case "error":
29
+ case "failure":
30
+ return "Error";
31
+ default:
32
+ // Convert snake_case to readable text
33
+ return reason.replace(/_/g, " ");
34
+ }
35
+ };
36
+ // Format time ago in a succinct way
37
+ const formatTimeAgo = (timestamp) => {
38
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
39
+ if (seconds < 60)
40
+ return `${seconds}s ago`;
41
+ const minutes = Math.floor(seconds / 60);
42
+ if (minutes < 60)
43
+ return `${minutes}m ago`;
44
+ const hours = Math.floor(minutes / 60);
45
+ if (hours < 24)
46
+ return `${hours}h ago`;
47
+ const days = Math.floor(hours / 24);
48
+ if (days < 30)
49
+ return `${days}d ago`;
50
+ const months = Math.floor(days / 30);
51
+ if (months < 12)
52
+ return `${months}mo ago`;
53
+ const years = Math.floor(months / 12);
54
+ return `${years}y ago`;
55
+ };
56
+ // Format duration in a succinct way
57
+ const formatDuration = (milliseconds) => {
58
+ const seconds = Math.floor(milliseconds / 1000);
59
+ if (seconds < 60)
60
+ return `${seconds}s`;
61
+ const minutes = Math.floor(seconds / 60);
62
+ if (minutes < 60)
63
+ return `${minutes}m`;
64
+ const hours = Math.floor(minutes / 60);
65
+ if (hours < 24)
66
+ return `${hours}h ${minutes % 60}m`;
67
+ const days = Math.floor(hours / 24);
68
+ return `${days}d ${hours % 24}h`;
69
+ };
70
+ // Capitalize first letter of a string
71
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
72
+ // Terminal states that don't need duration shown (no new state coming)
73
+ const TERMINAL_STATES = ["shutdown", "failure"];
74
+ export const StateHistory = ({ stateTransitions, shutdownReason, }) => {
75
+ if (!stateTransitions || stateTransitions.length === 0) {
76
+ return null;
77
+ }
78
+ // Check if there are more than 5 transitions
79
+ const totalTransitions = stateTransitions.length;
80
+ const hasMore = totalTransitions > 5;
81
+ // Get last 5 transitions (oldest first - chronological order)
82
+ const lastFive = stateTransitions
83
+ .slice(-5)
84
+ .map((transition, idx, arr) => {
85
+ const transitionTime = transition.transition_time_ms;
86
+ // Calculate duration: time until next transition, or until now if it's the last state
87
+ let duration = 0;
88
+ if (transitionTime) {
89
+ if (idx === arr.length - 1) {
90
+ // Most recent state - duration is from transition time to now
91
+ duration = Date.now() - transitionTime;
92
+ }
93
+ else {
94
+ // Earlier state - duration is from this transition to the next one
95
+ const nextTransition = arr[idx + 1];
96
+ const nextTransitionTime = nextTransition.transition_time_ms;
97
+ if (nextTransitionTime) {
98
+ duration = nextTransitionTime - transitionTime;
99
+ }
100
+ }
101
+ }
102
+ return {
103
+ status: transition.status,
104
+ transitionTime,
105
+ duration,
106
+ };
107
+ })
108
+ .filter((state) => state.transitionTime); // Only show states with valid timestamps
109
+ if (lastFive.length === 0) {
110
+ return null;
111
+ }
112
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: [figures.info, " State History", hasMore && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalTransitions - 5, " earlier)"] }))] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: lastFive.map((state, idx) => {
113
+ const statusDisplay = getStatusDisplay(state.status || "");
114
+ const isLastState = idx === lastFive.length - 1;
115
+ const isTerminalState = TERMINAL_STATES.includes(state.status);
116
+ const showDuration = state.duration > 0 && !(isLastState && isTerminalState);
117
+ const isShutdownState = state.status === "shutdown";
118
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: statusDisplay.color, children: [statusDisplay.icon, " "] }), _jsx(Text, { color: isLastState ? statusDisplay.color : colors.textDim, bold: isLastState, children: capitalize(state.status || "unknown") }), state.transitionTime && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [" ", "at ", new Date(state.transitionTime).toLocaleString(), " ", _jsxs(Text, { color: colors.textDim, children: ["(", formatTimeAgo(state.transitionTime), ")"] }), showDuration && (_jsxs(_Fragment, { children: [" ", "\u2022 Duration:", " ", _jsx(Text, { color: colors.secondary, children: formatDuration(state.duration) })] }))] }), isShutdownState && shutdownReason && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: " due to " }), _jsx(Text, { color: colors.warning, children: formatShutdownReason(shutdownReason) })] }))] }))] }, idx));
119
+ }) })] }));
120
+ };
@@ -8,87 +8,118 @@ export const getStatusDisplay = (status) => {
8
8
  icon: figures.questionMarkPrefix,
9
9
  color: colors.textDim,
10
10
  text: "UNKNOWN ",
11
+ label: "Unknown",
11
12
  };
12
13
  }
13
14
  switch (status) {
15
+ // === ACTIVE STATE ===
14
16
  case "running":
15
17
  return {
16
18
  icon: figures.circleFilled,
17
19
  color: colors.success,
18
20
  text: "RUNNING ",
21
+ label: "Running",
19
22
  };
23
+ // === STARTING UP (transitioning to active) ===
20
24
  case "provisioning":
21
25
  return {
22
- icon: figures.ellipsis,
26
+ icon: figures.arrowUp,
23
27
  color: colors.warning,
24
28
  text: "PROVISION ",
29
+ label: "Provisioning",
25
30
  };
26
31
  case "initializing":
27
32
  return {
28
- icon: figures.ellipsis,
33
+ icon: figures.arrowUp,
29
34
  color: colors.primary,
30
35
  text: "INITIALIZE",
36
+ label: "Initializing",
31
37
  };
38
+ case "resuming":
39
+ return {
40
+ icon: figures.arrowUp,
41
+ color: colors.primary,
42
+ text: "RESUMING ",
43
+ label: "Resuming",
44
+ };
45
+ // === SHUTTING DOWN (transitioning to inactive) ===
46
+ case "suspending":
47
+ return {
48
+ icon: figures.arrowDown,
49
+ color: colors.warning,
50
+ text: "SUSPENDING",
51
+ label: "Suspending",
52
+ };
53
+ // === INACTIVE STATES ===
32
54
  case "suspended":
33
55
  return {
34
56
  icon: figures.circleDotted,
35
57
  color: colors.warning,
36
58
  text: "SUSPENDED ",
59
+ label: "Suspended",
37
60
  };
38
- case "failure":
39
- return { icon: figures.cross, color: colors.error, text: "FAILED " };
40
61
  case "shutdown":
41
62
  return {
42
63
  icon: figures.circle,
43
64
  color: colors.textDim,
44
65
  text: "SHUTDOWN ",
66
+ label: "Shutdown",
45
67
  };
46
- case "resuming":
68
+ // === ERROR STATES ===
69
+ case "failure":
47
70
  return {
48
- icon: figures.ellipsis,
49
- color: colors.primary,
50
- text: "RESUMING ",
71
+ icon: figures.cross,
72
+ color: colors.error,
73
+ text: "FAILED ",
74
+ label: "Failed",
51
75
  };
52
- case "suspending":
76
+ case "build_failed":
77
+ case "failed":
53
78
  return {
54
- icon: figures.ellipsis,
55
- color: colors.warning,
56
- text: "SUSPENDING",
79
+ icon: figures.cross,
80
+ color: colors.error,
81
+ text: "FAILED ",
82
+ label: "Failed",
57
83
  };
84
+ // === BUILD STATES (for blueprints) ===
58
85
  case "ready":
59
86
  return {
60
- icon: figures.bullet,
87
+ icon: figures.tick,
61
88
  color: colors.success,
62
89
  text: "READY ",
90
+ label: "Ready",
63
91
  };
64
92
  case "build_complete":
65
93
  case "building_complete":
66
94
  return {
67
- icon: figures.bullet,
95
+ icon: figures.tick,
68
96
  color: colors.success,
69
97
  text: "COMPLETE ",
98
+ label: "Build Complete",
70
99
  };
71
100
  case "building":
72
101
  return {
73
- icon: figures.ellipsis,
102
+ icon: figures.arrowUp,
74
103
  color: colors.warning,
75
104
  text: "BUILDING ",
105
+ label: "Building: In Progress",
76
106
  };
77
- case "build_failed":
78
- case "failed":
79
- return { icon: figures.cross, color: colors.error, text: "FAILED " };
80
107
  default:
81
108
  // Truncate and pad any unknown status to 10 chars to match column width
82
109
  const truncated = status.toUpperCase().slice(0, 10);
83
110
  const padded = truncated.padEnd(10, " ");
111
+ // Capitalize first letter for label
112
+ const label = status.charAt(0).toUpperCase() + status.slice(1);
84
113
  return {
85
114
  icon: figures.questionMarkPrefix,
86
115
  color: colors.textDim,
87
116
  text: padded,
117
+ label: label,
88
118
  };
89
119
  }
90
120
  };
91
- export const StatusBadge = ({ status, showText = true }) => {
121
+ export const StatusBadge = ({ status, showText = true, fullText = false, }) => {
92
122
  const statusDisplay = getStatusDisplay(status);
93
- return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), showText && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: statusDisplay.color, children: statusDisplay.text })] }))] }));
123
+ const displayText = fullText ? statusDisplay.label : statusDisplay.text;
124
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), showText && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: statusDisplay.color, children: displayText })] }))] }));
94
125
  };
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useNavigation } from "../store/navigationStore.js";
3
3
  import { DevboxCreatePage } from "../components/DevboxCreatePage.js";
4
4
  export function DevboxCreateScreen() {
5
- const { goBack } = useNavigation();
6
- const handleCreate = () => {
7
- // After creation, go back to list (which will refresh)
8
- goBack();
5
+ const { goBack, navigate } = useNavigation();
6
+ const handleCreate = (devbox) => {
7
+ // After creation, navigate to the devbox detail page
8
+ navigate("devbox-detail", { devboxId: devbox.id });
9
9
  };
10
10
  return _jsx(DevboxCreatePage, { onBack: goBack, onCreate: handleCreate });
11
11
  }