diff --git a/src/components/DevboxDetailPage.tsx b/src/components/DevboxDetailPage.tsx index 78240186..5b04a7c5 100644 --- a/src/components/DevboxDetailPage.tsx +++ b/src/components/DevboxDetailPage.tsx @@ -6,6 +6,7 @@ import { StatusBadge } from "./StatusBadge.js"; import { MetadataDisplay } from "./MetadataDisplay.js"; import { Breadcrumb } from "./Breadcrumb.js"; import { DevboxActionsMenu } from "./DevboxActionsMenu.js"; +import { StateHistory } from "./StateHistory.js"; import { getDevboxUrl } from "../utils/url.js"; import { colors } from "../utils/theme.js"; import { useViewportHeight } from "../hooks/useViewportHeight.js"; @@ -840,6 +841,9 @@ export const DevboxDetailPage = ({ )} + {/* State History */} + + {/* Operations - inline display */} diff --git a/src/components/StateHistory.tsx b/src/components/StateHistory.tsx new file mode 100644 index 00000000..54498e35 --- /dev/null +++ b/src/components/StateHistory.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { Box, Text } from "ink"; +import figures from "figures"; +import { colors } from "../utils/theme.js"; + +interface StateHistoryProps { + stateTransitions?: any; +} + +// Format time ago in a succinct way +const formatTimeAgo = (timestamp: number): string => { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + + if (seconds < 60) return `${seconds}s ago`; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + + const months = Math.floor(days / 30); + if (months < 12) return `${months}mo ago`; + + const years = Math.floor(months / 12); + return `${years}y ago`; +}; + +// Format duration in a succinct way +const formatDuration = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + if (seconds < 60) return `${seconds}s`; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ${minutes % 60}m`; + + const days = Math.floor(hours / 24); + return `${days}d ${hours % 24}h`; +}; + +// Capitalize first letter of a string +const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + +export const StateHistory = ({ stateTransitions }: StateHistoryProps) => { + if ( + !stateTransitions || + !Array.isArray(stateTransitions) || + stateTransitions.length === 0 + ) { + return null; + } + + // Get last 3 transitions (most recent first) + const lastThree = ( + stateTransitions as Array<{ + status: string; + transition_time_ms?: number; + }> + ) + .slice(-3) + .reverse() + .map((transition, idx, arr) => { + const transitionTime = transition.transition_time_ms; + // Calculate duration: time until next transition, or until now if it's the current state + let duration = 0; + if (transitionTime) { + if (idx === 0) { + // Most recent state - duration is from transition time to now + duration = Date.now() - transitionTime; + } else { + // Previous state - duration is from this transition to the next one + const nextTransition = arr[idx - 1]; + const nextTransitionTime = nextTransition.transition_time_ms; + if (nextTransitionTime) { + duration = nextTransitionTime - transitionTime; + } + } + } + return { + status: transition.status, + transitionTime, + duration, + }; + }) + .filter((state) => state.transitionTime); // Only show states with valid timestamps + + if (lastThree.length === 0) { + return null; + } + + return ( + + + {figures.circleFilled} State History + + + {lastThree.map((state, idx) => ( + + + {capitalize(state.status)} + {state.transitionTime && ( + <> + {" "} + at {new Date(state.transitionTime).toLocaleString()}{" "} + + ({formatTimeAgo(state.transitionTime)}) + + {state.duration > 0 && ( + <> + {" "} + • Duration:{" "} + + {formatDuration(state.duration)} + + + )} + + )} + + + ))} + + + ); +}; diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 1d57bc02..3f091c09 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -43,7 +43,7 @@ const darkColors: ColorPalette = { // UI colors text: "#FFFFFF", // White - textDim: "#9CA3AF", // Gray + textDim: "#B8C2D0", // Brighter gray for better visibility in dark mode border: "#6B7280", // Medium gray background: "#000000", // Black