1- import React , { memo , useMemo , useState } from "react"
1+ import React , { memo , useCallback , useEffect , useMemo , useRef , useState } from "react"
22
33import { useAppTranslation } from "@/i18n/TranslationContext"
4+ import { vscode } from "@/utils/vscode"
45import type { StatsBucket } from "@roo-code/types"
56
67import { Button , StandardTooltip } from "@/components/ui"
@@ -13,10 +14,6 @@ interface DailyActivity {
1314 events : number
1415}
1516
16- interface UsageHeatmapProps {
17- buckets : StatsBucket [ ]
18- }
19-
2017// ── Heatmap color levels ────────────────────────────────────────────────────
2118
2219/**
@@ -79,15 +76,81 @@ const RANGE_OPTIONS: HeatmapRange[] = ["30d", "60d", "120d", "360d"]
7976
8077// ── UsageHeatmap ────────────────────────────────────────────────────────────
8178
82- const UsageHeatmap = memo ( ( { buckets } : UsageHeatmapProps ) => {
79+ const UsageHeatmap = memo ( ( ) => {
8380 const { t } = useAppTranslation ( )
8481 const [ range , setRange ] = useState < HeatmapRange > ( "30d" )
82+ const [ heatmapBuckets , setHeatmapBuckets ] = useState < StatsBucket [ ] > ( [ ] )
83+ const [ loading , setLoading ] = useState ( true )
84+ const latestHeatmapRequestIdRef = useRef < string > ( "" )
85+
86+ // Fetch heatmap data independently from the top-level date picker.
87+ // Sends a getUsageStats message with a "heatmap-" requestId prefix so
88+ // responses can be filtered from DashboardView's own requests.
89+ const fetchHeatmapData = useCallback ( ( rangeArg : HeatmapRange ) => {
90+ const days = RANGE_DAYS [ rangeArg ]
91+ const requestId = `heatmap-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } `
92+ latestHeatmapRequestIdRef . current = requestId
93+ setLoading ( true )
94+
95+ const from = new Date ( Date . now ( ) - days * 86400000 )
96+ from . setHours ( 0 , 0 , 0 , 0 )
97+
98+ let timezone : string
99+ try {
100+ timezone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone || "UTC"
101+ } catch {
102+ timezone = "UTC"
103+ }
104+
105+ vscode . postMessage ( {
106+ type : "getUsageStats" ,
107+ requestId,
108+ usageStatsQuery : {
109+ from : from . toISOString ( ) ,
110+ timezone,
111+ groupBy : [ "day" ] ,
112+ includeCancelled : false ,
113+ } ,
114+ } )
115+ } , [ ] )
116+
117+ // Listen for responses to our heatmap requests and perform initial fetch.
118+ useEffect ( ( ) => {
119+ const handleMessage = ( e : MessageEvent ) => {
120+ const message = e . data
121+
122+ if (
123+ message . type === "getUsageStatsResponse" &&
124+ typeof message . requestId === "string" &&
125+ message . requestId . startsWith ( "heatmap-" ) &&
126+ message . requestId === latestHeatmapRequestIdRef . current
127+ ) {
128+ if ( message . usageStatsSnapshot ) {
129+ setHeatmapBuckets ( message . usageStatsSnapshot . buckets ?? [ ] )
130+ }
131+ setLoading ( false )
132+ }
133+ }
134+
135+ window . addEventListener ( "message" , handleMessage )
136+ fetchHeatmapData ( range ) // Initial fetch
137+
138+ return ( ) => window . removeEventListener ( "message" , handleMessage )
139+ } , [ ] ) // eslint-disable-line react-hooks/exhaustive-deps
140+
141+ const handleRangeChange = useCallback (
142+ ( newRange : HeatmapRange ) => {
143+ setRange ( newRange )
144+ fetchHeatmapData ( newRange )
145+ } ,
146+ [ fetchHeatmapData ] ,
147+ )
85148
86149 // Extract daily activity from buckets that have a "day" key
87150 const dailyMap = useMemo ( ( ) => {
88151 const map = new Map < string , DailyActivity > ( )
89152
90- for ( const bucket of buckets ) {
153+ for ( const bucket of heatmapBuckets ) {
91154 const dayKey = bucket . key ?. day
92155 if ( ! dayKey ) continue
93156
@@ -105,7 +168,7 @@ const UsageHeatmap = memo(({ buckets }: UsageHeatmapProps) => {
105168 }
106169
107170 return map
108- } , [ buckets ] )
171+ } , [ heatmapBuckets ] )
109172
110173 // Generate the date range for display
111174 const days = useMemo ( ( ) => {
@@ -147,27 +210,25 @@ const UsageHeatmap = memo(({ buckets }: UsageHeatmapProps) => {
147210 return (
148211 < div className = "flex flex-col gap-2" data-testid = "usage-heatmap" >
149212 < div className = "flex items-center justify-between" >
150- < h4 className = "text-sm font-medium text-vscode-foreground m-0" >
151- { t ( "stats:heatmap.title" ) }
152- </ h4 >
213+ < h4 className = "text-sm font-medium text-vscode-foreground m-0" > { t ( "stats:heatmap.title" ) } </ h4 >
153214 < div className = "flex gap-1" >
154215 { RANGE_OPTIONS . map ( ( option ) => (
155216 < Button
156217 key = { option }
157218 variant = { range === option ? "primary" : "ghost" }
158219 size = "sm"
159- onClick = { ( ) => setRange ( option ) }
220+ onClick = { ( ) => handleRangeChange ( option ) }
160221 data-testid = { `heatmap-range-${ option } ` } >
161222 { t ( `stats:heatmap.${ option } ` ) }
162223 </ Button >
163224 ) ) }
164225 </ div >
165226 </ div >
166227
167- { ! hasData ? (
168- < div className = "text-xs text-vscode-descriptionForeground py-4" >
169- { t ( "stats:heatmap.noData" ) }
170- </ div >
228+ { loading && ! hasData ? (
229+ < div className = "text-xs text-vscode-descriptionForeground py-4" > { t ( "stats:heatmap.loading" ) } </ div >
230+ ) : ! hasData ? (
231+ < div className = "text-xs text-vscode-descriptionForeground py-4" > { t ( "stats:heatmap.noData" ) } < /div >
171232 ) : (
172233 < >
173234 < div
0 commit comments