@@ -24,6 +24,45 @@ const sidebarOpen = signal(false); // mobile sidebar drawer
2424
2525const activeRun = computed ( ( ) => runs . value . find ( r => r . run_id === activeRunId . value ) ) ;
2626
27+ // ── Time helpers ────────────────────────────────────────────────────
28+
29+ function formatElapsed ( startIso ) {
30+ if ( ! startIso ) return null ;
31+ const start = new Date ( startIso ) ;
32+ const now = new Date ( ) ;
33+ const seconds = Math . floor ( ( now - start ) / 1000 ) ;
34+ if ( seconds < 60 ) return `${ seconds } s` ;
35+ const minutes = Math . floor ( seconds / 60 ) ;
36+ if ( minutes < 60 ) return `${ minutes } m ${ seconds % 60 } s` ;
37+ const hours = Math . floor ( minutes / 60 ) ;
38+ return `${ hours } h ${ minutes % 60 } m` ;
39+ }
40+
41+ function formatTimeAgo ( iso ) {
42+ if ( ! iso ) return '' ;
43+ const date = new Date ( iso ) ;
44+ const now = new Date ( ) ;
45+ const seconds = Math . floor ( ( now - date ) / 1000 ) ;
46+ if ( seconds < 60 ) return 'just now' ;
47+ const minutes = Math . floor ( seconds / 60 ) ;
48+ if ( minutes < 60 ) return `${ minutes } m ago` ;
49+ const hours = Math . floor ( minutes / 60 ) ;
50+ if ( hours < 24 ) return `${ hours } h ago` ;
51+ const days = Math . floor ( hours / 24 ) ;
52+ if ( days === 1 ) return 'yesterday' ;
53+ if ( days < 7 ) return `${ days } d ago` ;
54+ return date . toLocaleDateString ( undefined , { month : 'short' , day : 'numeric' } ) ;
55+ }
56+
57+ function formatDateTime ( iso ) {
58+ if ( ! iso ) return '' ;
59+ const date = new Date ( iso ) ;
60+ return date . toLocaleString ( undefined , {
61+ month : 'short' , day : 'numeric' ,
62+ hour : '2-digit' , minute : '2-digit' ,
63+ } ) ;
64+ }
65+
2766function selectRun ( run_id ) {
2867 if ( run_id === activeRunId . value ) return ;
2968 activeRunId . value = run_id ;
@@ -71,13 +110,13 @@ function connectWs() {
71110}
72111
73112function handleEvent ( event ) {
74- const { type, run_id, data } = event ;
113+ const { type, run_id, data, timestamp } = event ;
75114
76115 if ( type === 'run_started' ) {
77116 const existing = runs . value . find ( r => r . run_id === run_id ) ;
78117 if ( existing ) {
79118 // Merge extra data (prompt_name, check counts, etc.) from the event
80- updateRun ( run_id , { status : 'running' , ...data } ) ;
119+ updateRun ( run_id , { status : 'running' , started_at : timestamp || existing . started_at , ...data } ) ;
81120 } else {
82121 runs . value = [ ...runs . value , {
83122 run_id,
@@ -86,6 +125,7 @@ function handleEvent(event) {
86125 completed : 0 ,
87126 failed : 0 ,
88127 timed_out : 0 ,
128+ started_at : timestamp ,
89129 ...data ,
90130 } ] ;
91131 }
@@ -149,7 +189,7 @@ function handleEvent(event) {
149189 const status = data . reason === 'completed' ? 'completed'
150190 : data . reason === 'error' ? 'failed'
151191 : 'stopped' ;
152- updateRun ( run_id , { status, ...data } ) ;
192+ updateRun ( run_id , { status, stopped_at : timestamp , ...data } ) ;
153193 // Mark any in-progress iterations as crashed/stopped
154194 if ( status !== 'completed' ) {
155195 const run = runs . value . find ( r => r . run_id === run_id ) ;
@@ -353,19 +393,28 @@ function Sidebar() {
353393}
354394
355395function RunCard ( { run } ) {
356- const isActive = activeRunId . value === run . run_id ;
396+ const isSelected = activeRunId . value === run . run_id ;
357397 const total = run . completed + run . failed ;
358398 const passRate = total > 0 ? ( run . completed / total ) * 100 : 0 ;
359399 const shortId = run . run_id . length > 8 ? run . run_id . slice ( 0 , 8 ) : run . run_id ;
360400 const displayTitle = run . prompt_name || shortId ;
401+ const isRunning = [ 'running' , 'paused' , 'pending' ] . includes ( run . status ) ;
402+
403+ // Live elapsed time for active runs
404+ const [ elapsed , setElapsed ] = useState ( ( ) => formatElapsed ( run . started_at ) ) ;
405+ useEffect ( ( ) => {
406+ if ( ! isRunning || ! run . started_at ) return ;
407+ const timer = setInterval ( ( ) => setElapsed ( formatElapsed ( run . started_at ) ) , 1000 ) ;
408+ return ( ) => clearInterval ( timer ) ;
409+ } , [ isRunning , run . started_at ] ) ;
361410
362411 return html `
363- < div class ="run-card ${ isActive ? 'active' : '' } " onClick =${ ( ) => { selectRun ( run . run_id ) ; sidebarOpen . value = false ; } } >
412+ < div class ="run-card ${ isSelected ? 'active' : '' } " onClick =${ ( ) => { selectRun ( run . run_id ) ; sidebarOpen . value = false ; } } >
364413 < div class ="run-badge ${ run . status } "> </ div >
365414 < div class ="run-card-info ">
366415 < div class ="run-card-title "> ${ displayTitle } </ div >
367416 < div class ="run-card-meta ">
368- ${ run . prompt_name ? shortId + ' · ' : '' } iter ${ run . iteration || 0 } ${ total > 0 ? ` · ${ Math . round ( passRate ) } %` : '' }
417+ ${ run . prompt_name ? shortId + ' · ' : '' } iter ${ run . iteration || 0 } ${ total > 0 ? ` · ${ Math . round ( passRate ) } %` : '' } ${ isRunning && elapsed ? ` · ${ elapsed } ` : '' }
369418 </ div >
370419 </ div >
371420 ${ total > 0 && html `
@@ -597,14 +646,42 @@ function RunOverview({ run }) {
597646 ? `Run completed with ${ passRate } % pass rate across ${ total } iterations.`
598647 : `Run ${ run . status } . ${ run . iteration || total } iteration${ ( run . iteration || total ) !== 1 ? 's' : '' } ran.` ;
599648
649+ const isActive = [ 'running' , 'paused' , 'pending' ] . includes ( run . status ) ;
650+
651+ // Live elapsed time for active runs
652+ const [ elapsed , setElapsed ] = useState ( formatElapsed ( run . started_at ) ) ;
653+ useEffect ( ( ) => {
654+ if ( ! isActive || ! run . started_at ) return ;
655+ const timer = setInterval ( ( ) => setElapsed ( formatElapsed ( run . started_at ) ) , 1000 ) ;
656+ return ( ) => clearInterval ( timer ) ;
657+ } , [ isActive , run . started_at ] ) ;
658+
659+ // For finished runs, show total duration if we have both timestamps
660+ const duration = ! isActive && run . started_at && run . stopped_at
661+ ? formatElapsed ( run . started_at ) // stopped_at - started_at would be better, but this shows total from start
662+ : null ;
663+
600664 return html `
601665 < div class ="run-overview ">
602666 < div class ="run-overview-header ">
603667 < div class ="run-overview-title ">
604668 < h2 > ${ run . prompt_name || 'Ad-hoc run' } </ h2 >
605669 < span class ="run-status-badge ${ run . status } "> ${ run . status } </ span >
606670 </ div >
607- < span style ="font-family: var(--font-mono); font-size: 12px; color: var(--text-muted) "> ${ run . run_id . length > 8 ? run . run_id . slice ( 0 , 8 ) : run . run_id } </ span >
671+ < div class ="run-overview-meta ">
672+ < span style ="font-family: var(--font-mono); font-size: 12px; color: var(--text-muted) "> ${ run . run_id . length > 8 ? run . run_id . slice ( 0 , 8 ) : run . run_id } </ span >
673+ ${ run . started_at && html `
674+ < span class ="run-overview-time ">
675+ < svg width ="12 " height ="12 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 " stroke-linecap ="round ">
676+ < circle cx ="12 " cy ="12 " r ="10 "/> < polyline points ="12 6 12 12 16 14 "/>
677+ </ svg >
678+ ${ isActive
679+ ? html `< span > Running for < strong > ${ elapsed } </ strong > </ span > `
680+ : html `< span title =${ formatDateTime ( run . started_at ) } > ${ formatTimeAgo ( run . started_at ) } </ span > `
681+ }
682+ </ span >
683+ ` }
684+ </ div >
608685 </ div >
609686 < div class ="run-overview-body ">
610687 < div class ="run-progress-ring ">
@@ -1416,6 +1493,10 @@ function HistoryView() {
14161493 < span class ="history-card-meta-id "> ${ shortId } </ span >
14171494 < span > \u00b7</ span >
14181495 < span > ${ r . iteration || total } iteration${ ( r . iteration || total ) !== 1 ? 's' : '' } </ span >
1496+ ${ r . started_at && html `
1497+ < span > \u00b7</ span >
1498+ < span class ="history-card-time " title =${ formatDateTime ( r . started_at ) } > ${ formatTimeAgo ( r . started_at ) } </ span >
1499+ ` }
14191500 < span class ="history-status-badge ${ r . status } "> ${ r . status } </ span >
14201501 </ div >
14211502 </ div >
0 commit comments