@@ -20,7 +20,9 @@ import {
2020 XCircle ,
2121 AlertCircle ,
2222 MessageSquare ,
23+ Clock ,
2324} from "lucide-react" ;
25+ import { Tooltip , TooltipContent , TooltipTrigger } from "../ui/tooltip" ;
2426import { cn } from "../cn" ;
2527import { Skeleton } from "../ui/skeleton" ;
2628import { UserHoverCard } from "../ui/user-hover-card" ;
@@ -875,10 +877,8 @@ export function Home() {
875877 ) }
876878 </ span >
877879 < div className = "flex items-center gap-2" >
878- { prList . lastFetchedAt && (
879- < span className = "text-[10px] text-muted-foreground" >
880- Updated { getTimeAgo ( new Date ( prList . lastFetchedAt ) ) }
881- </ span >
880+ { prList . lastFetchedAt && ! loadingPrs && (
881+ < RefreshCountdown lastFetchedAt = { prList . lastFetchedAt } />
882882 ) }
883883 < button
884884 onClick = { refreshPRList }
@@ -887,7 +887,7 @@ export function Home() {
887887 "p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground" ,
888888 loadingPrs && "opacity-50"
889889 ) }
890- title = "Refresh (auto-refreshes every 60s) "
890+ title = "Refresh"
891891 >
892892 < RefreshCw
893893 className = { cn ( "w-3.5 h-3.5" , loadingPrs && "animate-spin" ) }
@@ -1071,75 +1071,178 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
10711071 ? "Approval needed"
10721072 : "Running" ) ;
10731073
1074+ // Group checks by state for tooltip display
1075+ const checks = pr . ciChecks || [ ] ;
1076+ const successChecks = checks . filter ( ( c ) => c . state === "success" ) ;
1077+ const failureChecks = checks . filter ( ( c ) => c . state === "failure" ) ;
1078+ const pendingChecks = checks . filter (
1079+ ( c ) => c . state !== "success" && c . state !== "failure"
1080+ ) ;
1081+
1082+ const TooltipChecks = ( ) => (
1083+ < div className = "min-w-[200px] max-w-[300px]" >
1084+ < div className = "font-medium text-xs mb-2 pb-1.5 border-b border-border flex items-center gap-2" >
1085+ { pr . ciStatus === "success" && (
1086+ < >
1087+ < CheckCircle2 className = "w-3.5 h-3.5 text-green-500" />
1088+ < span > All checks passed</ span >
1089+ </ >
1090+ ) }
1091+ { pr . ciStatus === "failure" && (
1092+ < >
1093+ < XCircle className = "w-3.5 h-3.5 text-red-500" />
1094+ < span > Some checks failed</ span >
1095+ </ >
1096+ ) }
1097+ { pr . ciStatus === "pending" && (
1098+ < >
1099+ < Clock className = "w-3.5 h-3.5 text-yellow-500" />
1100+ < span > Checks in progress</ span >
1101+ </ >
1102+ ) }
1103+ { pr . ciStatus === "action_required" && (
1104+ < >
1105+ < AlertCircle className = "w-3.5 h-3.5 text-yellow-500" />
1106+ < span > Action required</ span >
1107+ </ >
1108+ ) }
1109+ </ div >
1110+ { checks . length > 0 ? (
1111+ < div className = "space-y-2" >
1112+ { /* Failed checks first */ }
1113+ { failureChecks . length > 0 && (
1114+ < div className = "space-y-1" >
1115+ { failureChecks . map ( ( c ) => (
1116+ < div
1117+ key = { c . name }
1118+ className = "flex items-center gap-2 text-[11px]"
1119+ >
1120+ < XCircle className = "w-3 h-3 text-red-500 shrink-0" />
1121+ < span className = "truncate text-red-400" > { c . name } </ span >
1122+ </ div >
1123+ ) ) }
1124+ </ div >
1125+ ) }
1126+ { /* Pending checks */ }
1127+ { pendingChecks . length > 0 && (
1128+ < div className = "space-y-1" >
1129+ { pendingChecks . map ( ( c ) => (
1130+ < div
1131+ key = { c . name }
1132+ className = "flex items-center gap-2 text-[11px]"
1133+ >
1134+ < Circle className = "w-3 h-3 text-yellow-500 shrink-0" />
1135+ < span className = "truncate text-muted-foreground" >
1136+ { c . name }
1137+ </ span >
1138+ </ div >
1139+ ) ) }
1140+ </ div >
1141+ ) }
1142+ { /* Successful checks (collapsed if many) */ }
1143+ { successChecks . length > 0 && (
1144+ < div className = "space-y-1" >
1145+ { successChecks . length <= 5 ? (
1146+ successChecks . map ( ( c ) => (
1147+ < div
1148+ key = { c . name }
1149+ className = "flex items-center gap-2 text-[11px]"
1150+ >
1151+ < CheckCircle2 className = "w-3 h-3 text-green-500 shrink-0" />
1152+ < span className = "truncate text-muted-foreground" >
1153+ { c . name }
1154+ </ span >
1155+ </ div >
1156+ ) )
1157+ ) : (
1158+ < div className = "flex items-center gap-2 text-[11px] text-muted-foreground" >
1159+ < CheckCircle2 className = "w-3 h-3 text-green-500 shrink-0" />
1160+ < span > { successChecks . length } checks passed</ span >
1161+ </ div >
1162+ ) }
1163+ </ div >
1164+ ) }
1165+ </ div >
1166+ ) : (
1167+ < div className = "text-[11px] text-muted-foreground" >
1168+ { pr . ciStatus === "action_required"
1169+ ? "Workflow approval required from a maintainer"
1170+ : "No detailed check information available" }
1171+ </ div >
1172+ ) }
1173+ </ div >
1174+ ) ;
1175+
1176+ const badgeContent = ( className : string , icon : React . ReactNode ) => (
1177+ < span
1178+ className = { cn (
1179+ "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded border cursor-default" ,
1180+ className
1181+ ) }
1182+ >
1183+ { icon }
1184+ < span className = "hidden sm:inline max-w-[100px] truncate" >
1185+ { summary }
1186+ </ span >
1187+ </ span >
1188+ ) ;
1189+
10741190 switch ( pr . ciStatus ) {
10751191 case "success" :
10761192 return (
1077- < span
1078- title = {
1079- pr . ciChecks
1080- ?. map (
1081- ( c ) =>
1082- `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
1083- )
1084- . join ( "\n" ) || "CI passed"
1085- }
1086- className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-green-500/15 text-green-500 border border-green-500/30"
1087- >
1088- < CheckCircle2 className = "w-3 h-3" />
1089- < span className = "hidden sm:inline max-w-[100px] truncate" >
1090- { summary }
1091- </ span >
1092- </ span >
1193+ < Tooltip >
1194+ < TooltipTrigger asChild >
1195+ { badgeContent (
1196+ "bg-green-500/15 text-green-500 border-green-500/30" ,
1197+ < CheckCircle2 className = "w-3 h-3" />
1198+ ) }
1199+ </ TooltipTrigger >
1200+ < TooltipContent side = "bottom" align = "start" >
1201+ < TooltipChecks />
1202+ </ TooltipContent >
1203+ </ Tooltip >
10931204 ) ;
10941205 case "failure" :
10951206 return (
1096- < span
1097- title = {
1098- pr . ciChecks
1099- ?. map (
1100- ( c ) =>
1101- `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
1102- )
1103- . join ( "\n" ) || "CI failed"
1104- }
1105- className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-red-500/15 text-red-500 border border-red-500/30"
1106- >
1107- < XCircle className = "w-3 h-3" />
1108- < span className = "hidden sm:inline max-w-[100px] truncate" >
1109- { summary }
1110- </ span >
1111- </ span >
1207+ < Tooltip >
1208+ < TooltipTrigger asChild >
1209+ { badgeContent (
1210+ "bg-red-500/15 text-red-500 border-red-500/30" ,
1211+ < XCircle className = "w-3 h-3" />
1212+ ) }
1213+ </ TooltipTrigger >
1214+ < TooltipContent side = "bottom" align = "start" >
1215+ < TooltipChecks />
1216+ </ TooltipContent >
1217+ </ Tooltip >
11121218 ) ;
11131219 case "pending" :
11141220 return (
1115- < span
1116- title = {
1117- pr . ciChecks
1118- ?. map (
1119- ( c ) =>
1120- `${ c . state === "success" ? "✓" : c . state === "failure" ? "✗" : "○" } ${ c . name } `
1121- )
1122- . join ( "\n" ) || "CI running"
1123- }
1124- className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-yellow-500/15 text-yellow-500 border border-yellow-500/30"
1125- >
1126- < Circle className = "w-3 h-3 animate-pulse" />
1127- < span className = "hidden sm:inline max-w-[100px] truncate" >
1128- { summary }
1129- </ span >
1130- </ span >
1221+ < Tooltip >
1222+ < TooltipTrigger asChild >
1223+ { badgeContent (
1224+ "bg-yellow-500/15 text-yellow-500 border-yellow-500/30" ,
1225+ < Circle className = "w-3 h-3 animate-pulse" />
1226+ ) }
1227+ </ TooltipTrigger >
1228+ < TooltipContent side = "bottom" align = "start" >
1229+ < TooltipChecks />
1230+ </ TooltipContent >
1231+ </ Tooltip >
11311232 ) ;
11321233 case "action_required" :
11331234 return (
1134- < span
1135- title = "Workflow approval required from a maintainer"
1136- className = "shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-yellow-500/15 text-yellow-500 border border-yellow-500/30"
1137- >
1138- < AlertCircle className = "w-3 h-3" />
1139- < span className = "hidden sm:inline max-w-[100px] truncate" >
1140- { summary }
1141- </ span >
1142- </ span >
1235+ < Tooltip >
1236+ < TooltipTrigger asChild >
1237+ { badgeContent (
1238+ "bg-yellow-500/15 text-yellow-500 border-yellow-500/30" ,
1239+ < AlertCircle className = "w-3 h-3" />
1240+ ) }
1241+ </ TooltipTrigger >
1242+ < TooltipContent side = "bottom" align = "start" >
1243+ < TooltipChecks />
1244+ </ TooltipContent >
1245+ </ Tooltip >
11431246 ) ;
11441247 default :
11451248 return null ;
@@ -1240,6 +1343,39 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
12401343 ) ;
12411344}
12421345
1346+ // ============================================================================
1347+ // Refresh Countdown
1348+ // ============================================================================
1349+
1350+ const REFRESH_INTERVAL_SECONDS = 60 ;
1351+
1352+ function RefreshCountdown ( { lastFetchedAt } : { lastFetchedAt : number } ) {
1353+ const [ secondsRemaining , setSecondsRemaining ] = useState ( ( ) => {
1354+ const elapsed = Math . floor ( ( Date . now ( ) - lastFetchedAt ) / 1000 ) ;
1355+ return Math . max ( 0 , REFRESH_INTERVAL_SECONDS - elapsed ) ;
1356+ } ) ;
1357+
1358+ useEffect ( ( ) => {
1359+ // Recalculate on mount or when lastFetchedAt changes
1360+ const elapsed = Math . floor ( ( Date . now ( ) - lastFetchedAt ) / 1000 ) ;
1361+ setSecondsRemaining ( Math . max ( 0 , REFRESH_INTERVAL_SECONDS - elapsed ) ) ;
1362+
1363+ const interval = setInterval ( ( ) => {
1364+ const elapsed = Math . floor ( ( Date . now ( ) - lastFetchedAt ) / 1000 ) ;
1365+ const remaining = Math . max ( 0 , REFRESH_INTERVAL_SECONDS - elapsed ) ;
1366+ setSecondsRemaining ( remaining ) ;
1367+ } , 1000 ) ;
1368+
1369+ return ( ) => clearInterval ( interval ) ;
1370+ } , [ lastFetchedAt ] ) ;
1371+
1372+ return (
1373+ < span className = "text-[10px] text-muted-foreground tabular-nums" >
1374+ Refreshing in { secondsRemaining } s
1375+ </ span >
1376+ ) ;
1377+ }
1378+
12431379// ============================================================================
12441380// Skeleton Components
12451381// ============================================================================
0 commit comments