From 280129b21202596583dcb4e62b083101594bb6b9 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Thu, 8 Jan 2026 15:55:11 -0800 Subject: [PATCH 1/2] cp dines --- src/components/DevboxDetailPage.tsx | 5 ++ src/components/StateHistory.tsx | 128 ++++++++++++++++++++++++++++ src/utils/theme.ts | 2 +- 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/components/StateHistory.tsx diff --git a/src/components/DevboxDetailPage.tsx b/src/components/DevboxDetailPage.tsx index 78240186..907aaf06 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"; @@ -40,6 +41,7 @@ const formatTimeAgo = (timestamp: number): string => { return `${years}y ago`; }; + export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, @@ -840,6 +842,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..f0e6fdee --- /dev/null +++ b/src/components/StateHistory.tsx @@ -0,0 +1,128 @@ +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 From bc560c88731bfffd9f43680f1de793523ede8d6a Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Thu, 8 Jan 2026 15:55:20 -0800 Subject: [PATCH 2/2] cp dines --- src/components/DevboxDetailPage.tsx | 1 - src/components/StateHistory.tsx | 23 +++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/DevboxDetailPage.tsx b/src/components/DevboxDetailPage.tsx index 907aaf06..5b04a7c5 100644 --- a/src/components/DevboxDetailPage.tsx +++ b/src/components/DevboxDetailPage.tsx @@ -41,7 +41,6 @@ const formatTimeAgo = (timestamp: number): string => { return `${years}y ago`; }; - export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, diff --git a/src/components/StateHistory.tsx b/src/components/StateHistory.tsx index f0e6fdee..54498e35 100644 --- a/src/components/StateHistory.tsx +++ b/src/components/StateHistory.tsx @@ -45,19 +45,24 @@ const formatDuration = (milliseconds: number): string => { }; // Capitalize first letter of a string -const capitalize = (str: string) => - str.charAt(0).toUpperCase() + str.slice(1); +const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); export const StateHistory = ({ stateTransitions }: StateHistoryProps) => { - if (!stateTransitions || !Array.isArray(stateTransitions) || stateTransitions.length === 0) { + 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; - }>) + const lastThree = ( + stateTransitions as Array<{ + status: string; + transition_time_ms?: number; + }> + ) .slice(-3) .reverse() .map((transition, idx, arr) => { @@ -102,9 +107,7 @@ export const StateHistory = ({ stateTransitions }: StateHistoryProps) => { {state.transitionTime && ( <> {" "} - at{" "} - {new Date(state.transitionTime).toLocaleString()} - {" "} + at {new Date(state.transitionTime).toLocaleString()}{" "} ({formatTimeAgo(state.transitionTime)})