@@ -168,6 +168,15 @@ pub struct Project {
168168 pub last_active : u64 ,
169169}
170170
171+ #[ derive( Debug , Serialize , Deserialize , Default ) ]
172+ pub struct SessionUsage {
173+ pub input_tokens : u64 ,
174+ pub output_tokens : u64 ,
175+ pub cache_creation_tokens : u64 ,
176+ pub cache_read_tokens : u64 ,
177+ pub cost_usd : f64 , // estimated cost in USD
178+ }
179+
171180#[ derive( Debug , Serialize , Deserialize ) ]
172181pub struct Session {
173182 pub id : String ,
@@ -176,6 +185,7 @@ pub struct Session {
176185 pub summary : Option < String > ,
177186 pub message_count : usize ,
178187 pub last_modified : u64 ,
188+ pub usage : Option < SessionUsage > ,
179189}
180190
181191#[ derive( Debug , Serialize , Deserialize ) ]
@@ -219,10 +229,19 @@ struct RawLine {
219229 is_meta : Option < bool > ,
220230}
221231
232+ #[ derive( Debug , Deserialize , Default ) ]
233+ struct RawUsage {
234+ input_tokens : Option < u64 > ,
235+ output_tokens : Option < u64 > ,
236+ cache_creation_input_tokens : Option < u64 > ,
237+ cache_read_input_tokens : Option < u64 > ,
238+ }
239+
222240#[ derive( Debug , Deserialize ) ]
223241struct RawMessage {
224242 role : Option < String > ,
225243 content : Option < serde_json:: Value > ,
244+ usage : Option < RawUsage > ,
226245}
227246
228247/// Entry from history.jsonl - used as fast session index
@@ -479,6 +498,7 @@ async fn list_sessions(project_id: String) -> Result<Vec<Session>, String> {
479498 summary,
480499 message_count,
481500 last_modified,
501+ usage : None ,
482502 } ) ;
483503 }
484504 }
@@ -490,6 +510,92 @@ async fn list_sessions(project_id: String) -> Result<Vec<Session>, String> {
490510 . map_err ( |e| e. to_string ( ) ) ?
491511}
492512
513+ /// Pricing constants (USD per 1M tokens) - Claude Opus 4.5 pricing as baseline
514+ const PRICE_INPUT_PER_M : f64 = 15.0 ;
515+ const PRICE_OUTPUT_PER_M : f64 = 75.0 ;
516+ const PRICE_CACHE_WRITE_PER_M : f64 = 3.75 ; // cache creation
517+ const PRICE_CACHE_READ_PER_M : f64 = 0.30 ; // cache read
518+
519+ /// Calculate cost from token counts
520+ fn calculate_cost ( usage : & SessionUsage ) -> f64 {
521+ let input_cost = ( usage. input_tokens as f64 / 1_000_000.0 ) * PRICE_INPUT_PER_M ;
522+ let output_cost = ( usage. output_tokens as f64 / 1_000_000.0 ) * PRICE_OUTPUT_PER_M ;
523+ let cache_write_cost = ( usage. cache_creation_tokens as f64 / 1_000_000.0 ) * PRICE_CACHE_WRITE_PER_M ;
524+ let cache_read_cost = ( usage. cache_read_tokens as f64 / 1_000_000.0 ) * PRICE_CACHE_READ_PER_M ;
525+ input_cost + output_cost + cache_write_cost + cache_read_cost
526+ }
527+
528+ /// Read usage data from a session file
529+ fn read_session_usage ( path : & Path ) -> SessionUsage {
530+ use std:: io:: { BufRead , BufReader } ;
531+
532+ let file = match fs:: File :: open ( path) {
533+ Ok ( f) => f,
534+ Err ( _) => return SessionUsage :: default ( ) ,
535+ } ;
536+
537+ let reader = BufReader :: new ( file) ;
538+ let mut usage = SessionUsage :: default ( ) ;
539+
540+ for line in reader. lines ( ) {
541+ let line = match line {
542+ Ok ( l) => l,
543+ Err ( _) => continue ,
544+ } ;
545+ if let Ok ( parsed) = serde_json:: from_str :: < RawLine > ( & line) {
546+ // Only assistant messages have usage data
547+ if parsed. line_type . as_deref ( ) == Some ( "assistant" ) {
548+ if let Some ( msg) = & parsed. message {
549+ if let Some ( u) = & msg. usage {
550+ usage. input_tokens += u. input_tokens . unwrap_or ( 0 ) ;
551+ usage. output_tokens += u. output_tokens . unwrap_or ( 0 ) ;
552+ usage. cache_creation_tokens += u. cache_creation_input_tokens . unwrap_or ( 0 ) ;
553+ usage. cache_read_tokens += u. cache_read_input_tokens . unwrap_or ( 0 ) ;
554+ }
555+ }
556+ }
557+ }
558+ }
559+
560+ usage. cost_usd = calculate_cost ( & usage) ;
561+ usage
562+ }
563+
564+ #[ derive( Debug , Serialize , Deserialize ) ]
565+ pub struct SessionUsageEntry {
566+ pub session_id : String ,
567+ pub usage : SessionUsage ,
568+ }
569+
570+ #[ tauri:: command]
571+ async fn get_sessions_usage ( project_id : String ) -> Result < Vec < SessionUsageEntry > , String > {
572+ tauri:: async_runtime:: spawn_blocking ( move || {
573+ let project_dir = get_claude_dir ( ) . join ( "projects" ) . join ( & project_id) ;
574+
575+ if !project_dir. exists ( ) {
576+ return Err ( "Project not found" . to_string ( ) ) ;
577+ }
578+
579+ let mut results = Vec :: new ( ) ;
580+
581+ for entry in fs:: read_dir ( & project_dir) . map_err ( |e| e. to_string ( ) ) ? {
582+ let entry = entry. map_err ( |e| e. to_string ( ) ) ?;
583+ let path = entry. path ( ) ;
584+ let name = path. file_name ( ) . unwrap ( ) . to_string_lossy ( ) . to_string ( ) ;
585+
586+ if name. ends_with ( ".jsonl" ) && !name. starts_with ( "agent-" ) {
587+ let session_id = name. trim_end_matches ( ".jsonl" ) . to_string ( ) ;
588+ let usage = read_session_usage ( & path) ;
589+ results. push ( SessionUsageEntry { session_id, usage } ) ;
590+ }
591+ }
592+
593+ Ok ( results)
594+ } )
595+ . await
596+ . map_err ( |e| e. to_string ( ) ) ?
597+ }
598+
493599/// Read only the first N lines of a session file to get summary (much faster than reading entire file)
494600fn read_session_head ( path : & Path , max_lines : usize ) -> ( Option < String > , usize ) {
495601 use std:: io:: { BufRead , BufReader } ;
@@ -690,6 +796,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
690796 summary : final_summary,
691797 message_count : head_msg_count, // approximate from head
692798 last_modified,
799+ usage : None ,
693800 } ) ;
694801 }
695802
@@ -736,6 +843,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
736843 summary,
737844 message_count : head_msg_count,
738845 last_modified,
846+ usage : None ,
739847 } ) ;
740848 }
741849 }
@@ -2493,6 +2601,7 @@ fn find_session_project(session_id: String) -> Result<Option<Session>, String> {
24932601 summary,
24942602 message_count : 0 ,
24952603 last_modified : 0 ,
2604+ usage : None ,
24962605 } ) ) ;
24972606 }
24982607 }
@@ -7260,6 +7369,7 @@ pub fn run() {
72607369 . invoke_handler ( tauri:: generate_handler![
72617370 list_projects,
72627371 list_sessions,
7372+ get_sessions_usage,
72637373 list_all_sessions,
72647374 list_all_chats,
72657375 get_session_messages,
0 commit comments