@@ -164,6 +164,114 @@ function buildSearchSessionListCases(
164164 return searchTerms . flatMap ( ( search ) => buildSessionListCases ( sessions , { search } ) ) ;
165165}
166166
167+ function usageCostTotals ( totalTokens : number , totalCost = 0 ) {
168+ return {
169+ input : Math . round ( totalTokens * 0.2 ) ,
170+ output : Math . round ( totalTokens * 0.1 ) ,
171+ cacheRead : Math . round ( totalTokens * 0.6 ) ,
172+ cacheWrite : Math . round ( totalTokens * 0.1 ) ,
173+ totalTokens,
174+ totalCost,
175+ inputCost : totalCost ,
176+ outputCost : 0 ,
177+ cacheReadCost : 0 ,
178+ cacheWriteCost : 0 ,
179+ missingCostEntries : 0 ,
180+ } ;
181+ }
182+
183+ // Deterministic year of daily activity so the settings profile heatmap,
184+ // streaks, and stat strip render with a lively fixture in the mock harness.
185+ function buildProfileUsageMocks ( baseTime : number ) {
186+ const daily : Array < Record < string , unknown > > = [ ] ;
187+ let lifetimeTokens = 0 ;
188+ for ( let daysAgo = 364 ; daysAgo >= 0 ; daysAgo -= 1 ) {
189+ const date = new Date ( baseTime - daysAgo * 24 * 60 * 60 * 1000 ) ;
190+ const iso = `${ date . getFullYear ( ) } -${ String ( date . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) } -${ String ( date . getDate ( ) ) . padStart ( 2 , "0" ) } ` ;
191+ const weekendDamper = date . getDay ( ) === 0 || date . getDay ( ) === 6 ? 0.3 : 1 ;
192+ const quietDay = daysAgo % 19 === 4 ? 0 : 1 ;
193+ const wave = ( Math . sin ( daysAgo / 6 ) + 1.4 ) * 1_400_000_000 ;
194+ const spike = daysAgo % 47 === 0 ? 6_000_000_000 : 0 ;
195+ const tokens = Math . round ( ( wave + spike ) * weekendDamper * quietDay ) ;
196+ lifetimeTokens += tokens ;
197+ daily . push ( { date : iso , ...usageCostTotals ( tokens , tokens / 1e9 ) } ) ;
198+ }
199+ return {
200+ cost : {
201+ updatedAt : baseTime ,
202+ days : daily . length ,
203+ daily,
204+ totals : usageCostTotals ( lifetimeTokens , lifetimeTokens / 1e9 ) ,
205+ } ,
206+ sessions : {
207+ updatedAt : baseTime ,
208+ startDate : daily [ 0 ] ?. date ,
209+ endDate : daily [ daily . length - 1 ] ?. date ,
210+ sessions : [
211+ {
212+ key : "agent:openclaw-mock:marathon" ,
213+ label : "Release night marathon" ,
214+ usage : { ...usageCostTotals ( 4_000_000_000 ) , durationMs : ( 59 * 60 + 4 ) * 60 * 1000 } ,
215+ } ,
216+ {
217+ key : "agent:openclaw-mock:daily" ,
218+ label : "Daily driver" ,
219+ usage : { ...usageCostTotals ( 900_000_000 ) , durationMs : 3 * 60 * 60 * 1000 } ,
220+ } ,
221+ ] ,
222+ totals : usageCostTotals ( lifetimeTokens , lifetimeTokens / 1e9 ) ,
223+ aggregates : {
224+ sessionCount : 48_212 ,
225+ longestSessionDurationMs : ( 59 * 60 + 4 ) * 60 * 1000 ,
226+ messages : {
227+ total : 2_787_815 ,
228+ user : 1_400_000 ,
229+ assistant : 1_387_815 ,
230+ toolCalls : 42_380 ,
231+ toolResults : 42_380 ,
232+ errors : 128 ,
233+ } ,
234+ tools : {
235+ totalCalls : 42_380 ,
236+ uniqueTools : 205 ,
237+ tools : [
238+ { name : "exec" , count : 6_418 } ,
239+ { name : "browser" , count : 5_256 } ,
240+ { name : "message" , count : 4_708 } ,
241+ { name : "read" , count : 4_489 } ,
242+ { name : "sessions_list" , count : 3_066 } ,
243+ ] ,
244+ } ,
245+ byModel : [
246+ {
247+ provider : "anthropic" ,
248+ model : "claude-sonnet-4-6" ,
249+ count : 9_000 ,
250+ totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.7 ) ) ,
251+ } ,
252+ {
253+ provider : "openai" ,
254+ model : "gpt-5.5" ,
255+ count : 4_000 ,
256+ totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.3 ) ) ,
257+ } ,
258+ ] ,
259+ byProvider : [ ] ,
260+ byAgent : [
261+ { agentId : "openclaw-mock" , totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.8 ) ) } ,
262+ { agentId : "alpha" , totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.2 ) ) } ,
263+ ] ,
264+ byChannel : [
265+ { channel : "whatsapp" , totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.5 ) ) } ,
266+ { channel : "telegram" , totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.3 ) ) } ,
267+ { channel : "discord" , totals : usageCostTotals ( Math . round ( lifetimeTokens * 0.2 ) ) } ,
268+ ] ,
269+ daily : [ ] ,
270+ } ,
271+ } ,
272+ } ;
273+ }
274+
167275function chatHistoryMessage ( role : "assistant" | "user" , text : string , timestamp : number ) {
168276 return {
169277 content : [ { text, type : "text" } ] ,
@@ -441,12 +549,17 @@ async function createChatPickerScenario(): Promise<ControlUiMockGatewayScenario>
441549 model : "claude-sonnet-4-6" ,
442550 modelProvider : "anthropic" ,
443551 } ) ;
552+ // Profile fixtures track the real clock so streaks and the trailing-year
553+ // heatmap stay filled no matter when the mock harness runs.
554+ const profileUsage = buildProfileUsageMocks ( Date . now ( ) ) ;
444555 return {
445556 assistantAgentId : "openclaw-mock" ,
446557 assistantName : "OpenClaw mock" ,
447558 defaultAgentId : "openclaw-mock" ,
448559 historyMessages : buildScrollableChatHistory ( baseTime ) ,
449560 methodResponses : {
561+ "usage.cost" : profileUsage . cost ,
562+ "sessions.usage" : profileUsage . sessions ,
450563 "device.pair.list" : { paired : [ ] , pending : [ ] } ,
451564 "device.pair.setupCode" : {
452565 auth : "token" ,
0 commit comments