@@ -4,7 +4,7 @@ import type { ChatMessage, ImageAttachment } from '@/components/chat';
44import type { ExtensionMessage , Model , ReactElementContext , ThinkingMode } from '@/types/protocol' ;
55
66import { useTauri } from '@/hooks/use-tauri' ;
7- import { conversationAddMessage } from '@/lib/backend' ;
7+ import { conversationAddMessage , conversationLoad } from '@/lib/backend' ;
88import { computeSimpleDiff , getLanguageFromPath } from '@/lib/diff-utils' ;
99import { useFileStore } from '@/stores/file-store' ;
1010import { useFileViewerStore } from '@/stores/file-viewer-store' ;
@@ -111,6 +111,7 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
111111 clearPermissions,
112112 addUsage,
113113 switchSession,
114+ restoreSessionUsage,
114115 } = useToolStore ( ) ;
115116 const { queueMessage : storeQueueMessage } = useQueuedMessageStore ( ) ;
116117
@@ -139,6 +140,81 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
139140 }
140141 } , [ sessionId ] ) ;
141142
143+ // Sync tool store's currentSessionId when React sessionId changes
144+ // This is critical for usage tracking - without this, usage accumulated before the first
145+ // switchSession call (e.g., from localStorage restore or system:init) won't be cached
146+ // when switching conversations, causing token counts to reset to 0.
147+ useEffect ( ( ) => {
148+ if ( sessionId ) {
149+ const toolState = useToolStore . getState ( ) ;
150+ if ( toolState . currentSessionId !== sessionId ) {
151+ switchSession ( sessionId ) ;
152+ }
153+ }
154+ } , [ sessionId , switchSession ] ) ;
155+
156+ // Restore usage from backend on initial mount (when sessionId comes from localStorage)
157+ // This ensures token counts persist across window reloads
158+ const hasRestoredUsage = useRef ( false ) ;
159+ const initialSessionId = useRef ( sessionId ) ; // Capture initial sessionId
160+ useEffect ( ( ) => {
161+ const sid = initialSessionId . current ;
162+ if ( ! sid || hasRestoredUsage . current ) return ;
163+
164+ // Only run once on mount with initial sessionId
165+ hasRestoredUsage . current = true ;
166+
167+ // Load conversation from backend to get persisted usage data
168+ void ( async ( ) : Promise < void > => {
169+ try {
170+ const conversation = await conversationLoad ( sid ) ;
171+ if ( ! conversation ?. messages ) return ;
172+
173+ // Calculate cumulative usage from all persisted messages
174+ const processedMessageIds : string [ ] = [ ] ;
175+ const cumulativeUsage = conversation . messages . reduce (
176+ ( acc , m ) => {
177+ if ( m . usage ) {
178+ processedMessageIds . push ( m . id ) ;
179+ return {
180+ inputTokens : acc . inputTokens + m . usage . inputTokens ,
181+ outputTokens : acc . outputTokens + m . usage . outputTokens ,
182+ cacheReadInputTokens :
183+ acc . cacheReadInputTokens + ( m . usage . cacheReadInputTokens ?? 0 ) ,
184+ cacheCreationInputTokens :
185+ acc . cacheCreationInputTokens + ( m . usage . cacheCreationInputTokens ?? 0 ) ,
186+ totalCostUsd : acc . totalCostUsd + ( m . usage . totalCostUsd ?? 0 ) ,
187+ } ;
188+ }
189+ return acc ;
190+ } ,
191+ {
192+ inputTokens : 0 ,
193+ outputTokens : 0 ,
194+ cacheReadInputTokens : 0 ,
195+ cacheCreationInputTokens : 0 ,
196+ totalCostUsd : 0 ,
197+ }
198+ ) ;
199+
200+ // Restore usage if we found any
201+ if ( processedMessageIds . length > 0 ) {
202+ // Use getState() to avoid stale closures
203+ const { restoreSessionUsage : restore , switchSession : sw } = useToolStore . getState ( ) ;
204+ restore ( sid , cumulativeUsage , processedMessageIds ) ;
205+ // Also update current session if it matches
206+ const toolState = useToolStore . getState ( ) ;
207+ if ( toolState . currentSessionId === sid ) {
208+ // Re-trigger switch to apply the restored usage
209+ sw ( sid ) ;
210+ }
211+ }
212+ } catch {
213+ // Ignore errors - conversation may not exist yet
214+ }
215+ } ) ( ) ;
216+ } , [ ] ) ; // Empty deps - only run on mount, uses refs for values
217+
142218 // Track if we have pending animations - only run interval when needed
143219 const hasAnimatingMessage = messages . some ( ( m ) => m . displayedContent . length < m . content . length ) ;
144220
@@ -267,13 +343,31 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
267343 : { } ) ,
268344 } ;
269345
270- // Persist assistant message to backend
346+ // Persist assistant message to backend (including usage for token tracking)
347+ // Build usage object conditionally to satisfy exactOptionalPropertyTypes
348+ const usageDto = message . usage
349+ ? {
350+ inputTokens : message . usage . input_tokens ,
351+ outputTokens : message . usage . output_tokens ,
352+ ...( message . usage . cache_read_input_tokens !== undefined
353+ ? { cacheReadInputTokens : message . usage . cache_read_input_tokens }
354+ : { } ) ,
355+ ...( message . usage . cache_creation_input_tokens !== undefined
356+ ? { cacheCreationInputTokens : message . usage . cache_creation_input_tokens }
357+ : { } ) ,
358+ ...( message . total_cost_usd !== undefined
359+ ? { totalCostUsd : message . total_cost_usd }
360+ : { } ) ,
361+ }
362+ : undefined ;
363+
271364 void conversationAddMessage ( message . session_id , {
272365 id : completedMsg . id ,
273366 role : 'assistant' ,
274367 content : completedMsg . content ,
275368 ...( completedMsg . thinking ? { thinking : completedMsg . thinking } : { } ) ,
276369 createdAt : Date . now ( ) ,
370+ ...( usageDto ? { usage : usageDto } : { } ) ,
277371 } ) ;
278372
279373 return [ ...prev . slice ( 0 , - 1 ) , completedMsg ] ;
@@ -376,14 +470,19 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
376470 }
377471
378472 case 'conversation:loaded' : {
379- // Prepare new messages
473+ // Prepare new messages (filter out system messages as they're not displayed)
380474 const cachedMessages = messagesCache . current . get ( message . session_id ) ;
381- const backendMessages = message . messages . map ( ( m ) => ( {
382- id : m . id ,
383- role : m . role ,
384- content : m . content ,
385- displayedContent : m . content ,
386- } ) ) ;
475+ const backendMessages = message . messages
476+ . filter (
477+ ( m ) : m is typeof m & { role : 'user' | 'assistant' } =>
478+ m . role === 'user' || m . role === 'assistant'
479+ )
480+ . map ( ( m ) => ( {
481+ id : m . id ,
482+ role : m . role ,
483+ content : m . content ,
484+ displayedContent : m . content ,
485+ } ) ) ;
387486
388487 // Determine which messages to use (prefer cache if it has more messages)
389488 let newMessages : typeof backendMessages ;
@@ -395,6 +494,39 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
395494 newMessages = backendMessages ;
396495 }
397496
497+ // Calculate cumulative usage from persisted messages (for token tracking persistence)
498+ const processedMessageIds : string [ ] = [ ] ;
499+ const cumulativeUsage = message . messages . reduce (
500+ ( acc , m ) => {
501+ if ( m . usage ) {
502+ processedMessageIds . push ( m . id ) ;
503+ return {
504+ inputTokens : acc . inputTokens + m . usage . inputTokens ,
505+ outputTokens : acc . outputTokens + m . usage . outputTokens ,
506+ cacheReadInputTokens :
507+ acc . cacheReadInputTokens + ( m . usage . cacheReadInputTokens ?? 0 ) ,
508+ cacheCreationInputTokens :
509+ acc . cacheCreationInputTokens + ( m . usage . cacheCreationInputTokens ?? 0 ) ,
510+ totalCostUsd : acc . totalCostUsd + ( m . usage . totalCostUsd ?? 0 ) ,
511+ } ;
512+ }
513+ return acc ;
514+ } ,
515+ {
516+ inputTokens : 0 ,
517+ outputTokens : 0 ,
518+ cacheReadInputTokens : 0 ,
519+ cacheCreationInputTokens : 0 ,
520+ totalCostUsd : 0 ,
521+ }
522+ ) ;
523+
524+ // Restore usage from persisted data BEFORE switching session
525+ // This ensures the session cache is pre-populated with correct usage
526+ if ( processedMessageIds . length > 0 ) {
527+ restoreSessionUsage ( message . session_id , cumulativeUsage , processedMessageIds ) ;
528+ }
529+
398530 // Set all state atomically - React 18 batches these updates
399531 setMessages ( newMessages ) ;
400532 setSessionId ( message . session_id ) ;
@@ -607,6 +739,7 @@ export function useChatMessages(options: UseChatMessagesOptions = {}): UseChatMe
607739 addPermissionRequest ,
608740 addUsage ,
609741 switchSession ,
742+ restoreSessionUsage ,
610743 onSessionCreated ,
611744 ]
612745 ) ;
0 commit comments