@@ -215,20 +215,42 @@ export function prepareVisibleAutomations<
215215 A extends {
216216 active : boolean ;
217217 created_at ?: string ;
218+ last_triggered_at ?: string | null ;
219+ stop_policy ?: { type : string } | null ;
218220 command_source ?: { type : string } | null ;
219221 }
220- > ( automations : A [ ] ) : A [ ] {
221- return automations
222- . filter (
223- ( a ) => ! ( a . command_source ?. type === 'native_loop' && ! a . active )
224- )
225- . slice ( )
226- . sort ( ( a , b ) => {
227- if ( a . active !== b . active ) return a . active ? - 1 : 1 ;
228- const aTime = a . created_at ? new Date ( a . created_at ) . getTime ( ) : 0 ;
229- const bTime = b . created_at ? new Date ( b . created_at ) . getTime ( ) : 0 ;
230- return bTime - aTime ;
231- } ) ;
222+ > ( automations : A [ ] ) : { live : A [ ] ; spent : A [ ] } {
223+ const sortByCreated = ( a : A , b : A ) => {
224+ if ( a . active !== b . active ) return a . active ? - 1 : 1 ;
225+ const aTime = a . created_at ? new Date ( a . created_at ) . getTime ( ) : 0 ;
226+ const bTime = b . created_at ? new Date ( b . created_at ) . getTime ( ) : 0 ;
227+ return bTime - aTime ;
228+ } ;
229+ const kept = automations . filter (
230+ ( a ) => ! ( a . command_source ?. type === 'native_loop' && ! a . active ) ,
231+ ) ;
232+ // "Spent" = an inactive automation that already fired at least once — almost
233+ // always a one-shot ScheduleWakeup the harness completed. These are history,
234+ // not actionable, and used to bury the single live driver under dozens of
235+ // dead rows. Anything still active, or inactive-but-never-fired (a paused or
236+ // failed-on-create automation worth seeing), stays in `live`.
237+ const live : A [ ] = [ ] ;
238+ const spent : A [ ] = [ ] ;
239+ for ( const a of kept ) {
240+ // Spent = a fired ONE-SHOT (stop_policy after_first_fire) that is now
241+ // inactive — i.e. a completed ScheduleWakeup. A user-paused recurring
242+ // automation (interval/webhook, different stop policy) is NOT spent even
243+ // though it has fired before; it stays in `live` so it remains editable.
244+ const isFiredOneShot =
245+ ! a . active &&
246+ ! ! a . last_triggered_at &&
247+ a . stop_policy ?. type === "after_first_fire" ;
248+ if ( isFiredOneShot ) spent . push ( a ) ;
249+ else live . push ( a ) ;
250+ }
251+ live . sort ( sortByCreated ) ;
252+ spent . sort ( sortByCreated ) ;
253+ return { live, spent } ;
232254}
233255
234256export function MissionAutomationsDialog ( {
@@ -250,6 +272,7 @@ export function MissionAutomationsDialog({
250272 const [ hasLoaded , setHasLoaded ] = useState ( false ) ;
251273 const [ loadedMissionId , setLoadedMissionId ] = useState < string | null > ( null ) ;
252274 const [ error , setError ] = useState < string | null > ( null ) ;
275+ const [ showSpent , setShowSpent ] = useState ( false ) ;
253276
254277 useEffect ( ( ) => {
255278 automationsRef . current = automations ;
@@ -916,7 +939,9 @@ export function MissionAutomationsDialog({
916939
917940 const isMissionDataReady = ! ! missionId && loadedMissionId === missionId ;
918941 const showLoadingPlaceholder = ! ! missionId && ( ! isMissionDataReady || ( loading && ! hasLoaded ) ) ;
919- const visibleAutomations = isMissionDataReady ? prepareVisibleAutomations ( automations ) : [ ] ;
942+ const { live : visibleAutomations , spent : spentAutomations } = isMissionDataReady
943+ ? prepareVisibleAutomations ( automations )
944+ : { live : [ ] , spent : [ ] } ;
920945 const visibleError = isMissionDataReady ? error : null ;
921946 const selectClass =
922947 'rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50 appearance-none cursor-pointer' ;
@@ -1422,6 +1447,7 @@ export function MissionAutomationsDialog({
14221447 { isMissionDataReady &&
14231448 ! loading &&
14241449 visibleAutomations . length === 0 &&
1450+ spentAutomations . length === 0 &&
14251451 ! visibleError && (
14261452 < div className = "rounded-xl border border-white/[0.06] bg-white/[0.02] p-6 text-center text-sm text-white/40" >
14271453 No automations yet. Create one above.
@@ -1703,6 +1729,39 @@ export function MissionAutomationsDialog({
17031729 ) ;
17041730 } ) }
17051731 </ div >
1732+
1733+ { spentAutomations . length > 0 && (
1734+ < div className = "mt-3 border-t border-white/[0.06] pt-2" >
1735+ < button
1736+ type = "button"
1737+ onClick = { ( ) => setShowSpent ( ( v ) => ! v ) }
1738+ className = "flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-[11px] text-white/40 hover:bg-white/[0.04] hover:text-white/60"
1739+ >
1740+ < span >
1741+ { showSpent ? 'Hide' : 'Show' } { spentAutomations . length } completed
1742+ { ' ' } wakeup{ spentAutomations . length === 1 ? '' : 's' } (history)
1743+ </ span >
1744+ < span > { showSpent ? '▾' : '▸' } </ span >
1745+ </ button >
1746+ { showSpent && (
1747+ < div className = "mt-1 space-y-1" >
1748+ { spentAutomations . map ( ( automation ) => (
1749+ < div
1750+ key = { automation . id }
1751+ className = "flex items-center justify-between gap-2 rounded-md px-2 py-1 text-[11px] text-white/35"
1752+ >
1753+ < span className = "truncate" > { getAutomationLabel ( automation ) } </ span >
1754+ < span className = "shrink-0 text-white/25" >
1755+ { automation . last_triggered_at
1756+ ? new Date ( automation . last_triggered_at ) . toLocaleString ( )
1757+ : '' }
1758+ </ span >
1759+ </ div >
1760+ ) ) }
1761+ </ div >
1762+ ) }
1763+ </ div >
1764+ ) }
17061765 </ div >
17071766 </ >
17081767 ) }
0 commit comments