11'use client'
22
3+ import { useRouterState } from '@tanstack/react-router'
34import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
45import { UsageDetailsModal } from './usage-details-modal'
56import { ContextAlertModal } from './context-alert-modal'
7+ import {
8+ resolveContextAlertThreshold ,
9+ resolveUsageMeterSessionKey ,
10+ shouldShowUsageMeterContextAlert ,
11+ } from './usage-meter-session'
612import { DialogContent , DialogRoot } from '@/components/ui/dialog'
713import {
814 MenuContent ,
@@ -434,7 +440,16 @@ type AgentActivity = {
434440 totalAgentCost : number
435441}
436442
437- export function UsageMeter ( ) {
443+ export function UsageMeter ( { visible = true } : { visible ?: boolean } ) {
444+ const pathname = useRouterState ( { select : ( state ) => state . location . pathname } )
445+ const statusSessionKey = useMemo (
446+ ( ) => resolveUsageMeterSessionKey ( pathname ) ,
447+ [ pathname ] ,
448+ )
449+ const contextAlertsEnabled = useMemo (
450+ ( ) => shouldShowUsageMeterContextAlert ( { pathname, visible } ) ,
451+ [ pathname , visible ] ,
452+ )
438453 const [ usage , setUsage ] = useState < UsageSummary > ( ( ) =>
439454 parseSessionStatus ( null ) ,
440455 )
@@ -458,10 +473,14 @@ export function UsageMeter() {
458473 threshold : number
459474 } > ( { open : false , threshold : 0 } )
460475 const alertStateRef = useRef ( getAlertState ( ) )
476+ const previousContextPercentRef = useRef < number | null > ( null )
461477
462478 const refresh = useCallback ( async ( ) => {
463479 try {
464- const res = await fetch ( '/api/session-status' )
480+ const query = statusSessionKey
481+ ? `?sessionKey=${ encodeURIComponent ( statusSessionKey ) } `
482+ : ''
483+ const res = await fetch ( `/api/session-status${ query } ` )
465484 if ( ! res . ok ) {
466485 const data = await res . json ( ) . catch ( ( ) => null )
467486 throw new Error (
@@ -478,7 +497,7 @@ export function UsageMeter() {
478497 setError ( errorMessage )
479498 toast ( 'Failed to fetch usage data' , { type : 'error' } )
480499 }
481- } , [ ] )
500+ } , [ statusSessionKey ] )
482501
483502 const refreshProviders = useCallback ( async ( ) => {
484503 try {
@@ -546,28 +565,43 @@ export function UsageMeter() {
546565 } , [ refreshAgentActivity ] )
547566
548567 useEffect ( ( ) => {
568+ if ( ! contextAlertsEnabled && contextAlert . open ) {
569+ setContextAlert ( { open : false , threshold : 0 } )
570+ }
571+ } , [ contextAlert . open , contextAlertsEnabled ] )
572+
573+ useEffect ( ( ) => {
574+ if ( ! contextAlertsEnabled ) {
575+ previousContextPercentRef . current = usage . contextPercent
576+ return
577+ }
549578 if ( typeof window === 'undefined' ) return
550579 const current = usage . contextPercent
551580 if ( ! Number . isFinite ( current ) ) return
581+ const previous = previousContextPercentRef . current
582+ previousContextPercentRef . current = current
552583 const state = alertStateRef . current
553584 if ( state . date !== getTodayKey ( ) ) {
554585 state . date = getTodayKey ( )
555586 state . sent = { }
556587 }
557- const eligible = THRESHOLDS . filter ( ( threshold ) => current >= threshold )
558- if ( eligible . length === 0 ) return
559- for ( const threshold of eligible ) {
560- if ( state . sent [ threshold ] ) continue
561- state . sent [ threshold ] = true
562- saveAlertState ( state )
563- // Show in-app modal instead of browser notification
564- setContextAlert ( { open : true , threshold } )
565- break // Only show one alert at a time
566- }
567- } , [ usage . contextPercent ] )
588+ const threshold = resolveContextAlertThreshold ( {
589+ previous,
590+ current,
591+ thresholds : THRESHOLDS ,
592+ sent : state . sent ,
593+ } )
594+ if ( ! threshold ) return
595+ state . sent [ threshold ] = true
596+ saveAlertState ( state )
597+ // Show in-app modal instead of browser notification
598+ setContextAlert ( { open : true , threshold } )
599+ } , [ contextAlertsEnabled , usage . contextPercent ] )
568600
569601 useEffect ( ( ) => {
570602 function handleOpenUsageFromSearch ( ) {
603+ void refresh ( )
604+ void refreshProviders ( )
571605 setOpen ( true )
572606 }
573607
@@ -581,7 +615,7 @@ export function UsageMeter() {
581615 handleOpenUsageFromSearch ,
582616 )
583617 }
584- } , [ ] )
618+ } , [ refresh , refreshProviders ] )
585619
586620 // Find the preferred provider for the status bar display
587621 const [ preferredProvider , setPreferredProvider ] = useState < string | null > (
@@ -839,39 +873,41 @@ export function UsageMeter() {
839873
840874 return (
841875 < >
842- < MenuRoot >
843- < MenuTrigger
844- className = { cn (
845- "absolute bottom-2 right-2" ,
846- 'ml-auto rounded-full border px-3 py-1 text-xs font-medium' ,
847- 'flex items-center gap-3 transition hover:bg-primary-100 cursor-pointer' ,
848- alertTone ,
849- ) }
850- data-tour = "usage-meter"
851- >
852- < span className = "text-[9px] uppercase tracking-widest text-primary-500 opacity-75" >
853- { STATS_VIEW_LABELS [ statsView ] . split ( ' ' ) [ 0 ] }
854- </ span >
855- < span className = "text-primary-300" > |</ span >
856- { renderPillContent ( ) }
857- </ MenuTrigger >
858- < MenuContent align = "end" className = "min-w-[180px]" >
859- { ( [ 'session' , 'provider' , 'cost' , 'agents' ] as const ) . map ( ( view ) => (
860- < MenuItem
861- key = { view }
862- onClick = { ( ) => handleStatsViewChange ( view ) }
863- className = { cn (
864- statsView === view && 'bg-amber-100 text-amber-800' ,
865- ) }
866- >
867- < span className = "flex-1" > { STATS_VIEW_LABELS [ view ] } </ span >
868- { statsView === view && < span className = "text-amber-600" > ✓</ span > }
869- </ MenuItem >
870- ) ) }
871- < div className = "my-1 h-px bg-primary-100" />
872- < MenuItem onClick = { ( ) => setOpen ( true ) } > View Details…</ MenuItem >
873- </ MenuContent >
874- </ MenuRoot >
876+ { visible ? (
877+ < MenuRoot >
878+ < MenuTrigger
879+ className = { cn (
880+ "absolute bottom-2 right-2" ,
881+ 'ml-auto rounded-full border px-3 py-1 text-xs font-medium' ,
882+ 'flex items-center gap-3 transition hover:bg-primary-100 cursor-pointer' ,
883+ alertTone ,
884+ ) }
885+ data-tour = "usage-meter"
886+ >
887+ < span className = "text-[9px] uppercase tracking-widest text-primary-500 opacity-75" >
888+ { STATS_VIEW_LABELS [ statsView ] . split ( ' ' ) [ 0 ] }
889+ </ span >
890+ < span className = "text-primary-300" > |</ span >
891+ { renderPillContent ( ) }
892+ </ MenuTrigger >
893+ < MenuContent align = "end" className = "min-w-[180px]" >
894+ { ( [ 'session' , 'provider' , 'cost' , 'agents' ] as const ) . map ( ( view ) => (
895+ < MenuItem
896+ key = { view }
897+ onClick = { ( ) => handleStatsViewChange ( view ) }
898+ className = { cn (
899+ statsView === view && 'bg-amber-100 text-amber-800' ,
900+ ) }
901+ >
902+ < span className = "flex-1" > { STATS_VIEW_LABELS [ view ] } </ span >
903+ { statsView === view && < span className = "text-amber-600" > ✓</ span > }
904+ </ MenuItem >
905+ ) ) }
906+ < div className = "my-1 h-px bg-primary-100" />
907+ < MenuItem onClick = { ( ) => setOpen ( true ) } > View Details…</ MenuItem >
908+ </ MenuContent >
909+ </ MenuRoot >
910+ ) : null }
875911
876912 < DialogRoot open = { open } onOpenChange = { setOpen } >
877913 < DialogContent className = "w-[min(720px,94vw)]" >
0 commit comments