@@ -8,7 +8,7 @@ interface ConversationContextType {
88 allSources : SourceMemory [ ] ;
99 isLoadingMessages : boolean ;
1010 pendingMessage : string | null ;
11- contextUsage : TokenUsage | null ; // Latest message only (for context window % )
11+ estimatedTokens : number ; // Estimated conversation tokens (stable, grows with messages )
1212 billingUsage : TokenUsage | null ; // Cumulative (for cost tracking)
1313 contextWindow : number ;
1414 selectConversation : ( conversation : Conversation | null ) => void ;
@@ -18,42 +18,34 @@ interface ConversationContextType {
1818 updateMessage : ( id : string | number , updates : Partial < ChatMessage > ) => void ;
1919 clearMessages : ( ) => void ;
2020 setPendingMessage : ( message : string | null ) => void ;
21- updateUsage : ( usage : TokenUsage | null , contextWindow ? : number ) => void ;
21+ updateContextWindow : ( contextWindow : number ) => void ;
2222}
2323
2424const ConversationContext = createContext < ConversationContextType | null > ( null ) ;
2525
26+ // Estimate tokens from text (~4 chars per token is a common approximation)
27+ const SYSTEM_PROMPT_TOKENS = 80 ; // Approximate tokens for the system prompt
28+
29+ function estimateTokens ( text : string ) : number {
30+ return Math . ceil ( text . length / 4 ) ;
31+ }
32+
2633export function ConversationProvider ( { children } : { children : ReactNode } ) {
2734 const [ currentConversationId , setCurrentConversationId ] = useState < number | null > ( null ) ;
2835 const [ messages , setMessages ] = useState < ChatMessage [ ] > ( [ ] ) ;
2936 const [ isLoadingMessages , setIsLoadingMessages ] = useState ( false ) ;
3037 const [ pendingMessage , setPendingMessage ] = useState < string | null > ( null ) ;
31- const [ contextUsage , setContextUsage ] = useState < TokenUsage | null > ( null ) ; // Latest only
3238 const [ billingUsage , setBillingUsage ] = useState < TokenUsage | null > ( null ) ; // Cumulative
3339 const [ contextWindow , setContextWindow ] = useState ( 128000 ) ;
3440
35- const updateUsage = useCallback ( ( newUsage : TokenUsage | null , newContextWindow ?: number ) => {
36- if ( newUsage ) {
37- // Context usage = latest message only (for context window %)
38- setContextUsage ( newUsage ) ;
39-
40- // Billing usage = accumulate across conversation (for cost tracking)
41- setBillingUsage ( ( prev ) =>
42- prev
43- ? {
44- prompt_tokens : prev . prompt_tokens + newUsage . prompt_tokens ,
45- completion_tokens : prev . completion_tokens + newUsage . completion_tokens ,
46- total_tokens : prev . total_tokens + newUsage . total_tokens ,
47- }
48- : newUsage
49- ) ;
50- } else {
51- setContextUsage ( null ) ;
52- setBillingUsage ( null ) ;
53- }
54- if ( newContextWindow ) {
55- setContextWindow ( newContextWindow ) ;
56- }
41+ // Estimate conversation tokens from messages (stable, grows with conversation)
42+ const estimatedTokens = useMemo ( ( ) => {
43+ const messageTokens = messages . reduce ( ( acc , msg ) => acc + estimateTokens ( msg . content ) , 0 ) ;
44+ return SYSTEM_PROMPT_TOKENS + messageTokens ;
45+ } , [ messages ] ) ;
46+
47+ const updateContextWindow = useCallback ( ( newContextWindow : number ) => {
48+ setContextWindow ( newContextWindow ) ;
5749 } , [ ] ) ;
5850
5951 const loadConversation = useCallback ( async ( conversationId : number ) => {
@@ -70,24 +62,10 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
7062 } ) )
7163 ) ;
7264
73- // Get usage from messages
65+ // Billing usage = sum of all assistant message tokens (for cost tracking)
7466 const assistantMessages = data . messages . filter (
7567 ( m ) => m . role === "assistant" && m . total_tokens
7668 ) ;
77-
78- // Context usage = last assistant message only (current context window state)
79- const lastAssistant = assistantMessages [ assistantMessages . length - 1 ] ;
80- if ( lastAssistant ) {
81- setContextUsage ( {
82- prompt_tokens : lastAssistant . prompt_tokens || 0 ,
83- completion_tokens : lastAssistant . completion_tokens || 0 ,
84- total_tokens : lastAssistant . total_tokens || 0 ,
85- } ) ;
86- } else {
87- setContextUsage ( null ) ;
88- }
89-
90- // Billing usage = sum of all (total tokens consumed)
9169 const totalBilling = assistantMessages . reduce (
9270 ( acc , m ) => ( {
9371 prompt_tokens : acc . prompt_tokens + ( m . prompt_tokens || 0 ) ,
@@ -110,7 +88,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
11088 }
11189 } , [ ] ) ;
11290
113- // Aggregate all sources from messages , deduplicated by id
91+ // Aggregate all sources from conversation , deduplicated by id
11492 const allSources = useMemo ( ( ) => {
11593 const sourceMap = new Map < number , SourceMemory > ( ) ;
11694 for ( const msg of messages ) {
@@ -133,7 +111,6 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
133111 } else {
134112 setCurrentConversationId ( null ) ;
135113 setMessages ( [ ] ) ;
136- setContextUsage ( null ) ;
137114 setBillingUsage ( null ) ;
138115 }
139116 } ,
@@ -143,7 +120,6 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
143120 const startNewChat = useCallback ( ( ) => {
144121 setCurrentConversationId ( null ) ;
145122 setMessages ( [ ] ) ;
146- setContextUsage ( null ) ;
147123 setBillingUsage ( null ) ;
148124 } , [ ] ) ;
149125
@@ -169,7 +145,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
169145 allSources,
170146 isLoadingMessages,
171147 pendingMessage,
172- contextUsage ,
148+ estimatedTokens ,
173149 billingUsage,
174150 contextWindow,
175151 selectConversation,
@@ -179,7 +155,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
179155 updateMessage,
180156 clearMessages,
181157 setPendingMessage,
182- updateUsage ,
158+ updateContextWindow ,
183159 } }
184160 >
185161 { children }
0 commit comments