@@ -7,9 +7,9 @@ import { ContextFileItem } from "../../components/ContextFileItem";
77import { useAtom } from "jotai" ;
88import { sessionContextTabAtom , sessionSelectModeAtom , hideEmptySessionsAtom , userPromptsOnlyAtom } from "../../store" ;
99import { useAppConfig } from "../../context" ;
10- import { formatDate } from "./utils" ;
10+ import { formatDate , useReadableText } from "./utils" ;
1111import { useInvokeQuery } from "../../hooks" ;
12- import type { Session , ContextFile , Message , SearchResult } from "../../types" ;
12+ import type { Session , ContextFile , Message , SearchResult , SessionUsageEntry , SessionUsage } from "../../types" ;
1313
1414interface SessionListProps {
1515 projectId : string ;
@@ -18,8 +18,22 @@ interface SessionListProps {
1818 onSelect : ( s : Session ) => void ;
1919}
2020
21+ // Format token count (e.g., 1234567 -> "1.23M")
22+ function formatTokens ( n : number ) : string {
23+ if ( n >= 1_000_000 ) return `${ ( n / 1_000_000 ) . toFixed ( 2 ) } M` ;
24+ if ( n >= 1_000 ) return `${ ( n / 1_000 ) . toFixed ( 1 ) } K` ;
25+ return n . toString ( ) ;
26+ }
27+
28+ // Format cost (e.g., 0.1234 -> "$0.12")
29+ function formatCost ( cost : number ) : string {
30+ if ( cost < 0.01 ) return `$${ cost . toFixed ( 4 ) } ` ;
31+ return `$${ cost . toFixed ( 2 ) } ` ;
32+ }
33+
2134export function SessionList ( { projectId, projectPath, onBack, onSelect } : SessionListProps ) {
2235 const { formatPath } = useAppConfig ( ) ;
36+ const toReadable = useReadableText ( ) ;
2337
2438 // Use react-query for cached data
2539 const { data : sessions = [ ] , isLoading : loadingSessions } = useInvokeQuery < Session [ ] > (
@@ -37,6 +51,33 @@ export function SessionList({ projectId, projectPath, onBack, onSelect }: Sessio
3751 { projectPath } ,
3852 ) ;
3953
54+ // Load usage data separately (lazy loading for performance)
55+ const { data : usageData = [ ] } = useInvokeQuery < SessionUsageEntry [ ] > (
56+ [ "sessionsUsage" , projectId ] ,
57+ "get_sessions_usage" ,
58+ { projectId }
59+ ) ;
60+
61+ // Create usage map for quick lookup
62+ const usageMap = useMemo ( ( ) => {
63+ const map = new Map < string , SessionUsage > ( ) ;
64+ usageData . forEach ( ( entry ) => map . set ( entry . session_id , entry . usage ) ) ;
65+ return map ;
66+ } , [ usageData ] ) ;
67+
68+ // Calculate total usage for all sessions
69+ const totalUsage = useMemo ( ( ) => {
70+ let total = { input_tokens : 0 , output_tokens : 0 , cache_creation_tokens : 0 , cache_read_tokens : 0 , cost_usd : 0 } ;
71+ usageData . forEach ( ( entry ) => {
72+ total . input_tokens += entry . usage . input_tokens ;
73+ total . output_tokens += entry . usage . output_tokens ;
74+ total . cache_creation_tokens += entry . usage . cache_creation_tokens ;
75+ total . cache_read_tokens += entry . usage . cache_read_tokens ;
76+ total . cost_usd += entry . usage . cost_usd ;
77+ } ) ;
78+ return total ;
79+ } , [ usageData ] ) ;
80+
4081 const globalContext = useMemo ( ( ) => allContextFiles . filter ( ( f ) => f . scope === "global" ) , [ allContextFiles ] ) ;
4182 const loading = loadingSessions || loadingContext ;
4283
@@ -108,7 +149,7 @@ generator: "Lovcode"
108149 . replace ( / \s + / g, "-" ) ;
109150 const toc = selected
110151 . map ( ( s , i ) => {
111- const title = `Session ${ i + 1 } : ${ s . summary || "Untitled" } ` ;
152+ const title = `Session ${ i + 1 } : ${ toReadable ( s . summary ) || "Untitled" } ` ;
112153 return `- [${ title } ](#${ toAnchor ( title ) } )` ;
113154 } )
114155 . join ( "\n" ) ;
@@ -126,7 +167,7 @@ generator: "Lovcode"
126167 . join ( "\n\n---\n\n" ) ;
127168 const msgCountLabel = userPromptsOnly ? `${ messages . length } prompts` : `${ session . message_count } messages` ;
128169 const meta = `_${ msgCountLabel } · ${ formatDate ( session . last_modified ) } _` ;
129- parts . push ( `## Session ${ i + 1 } : ${ session . summary || "Untitled" } \n\n${ meta } \n\n${ sessionMd } ` ) ;
170+ parts . push ( `## Session ${ i + 1 } : ${ toReadable ( session . summary ) || "Untitled" } \n\n${ meta } \n\n${ sessionMd } ` ) ;
130171 }
131172 const body = parts . join ( "\n\n<br>\n\n---\n\n<br>\n\n" ) ;
132173 const header = `# ${ projectName }
@@ -162,9 +203,21 @@ generator: "Lovcode"
162203 < button onClick = { onBack } className = "text-muted-foreground hover:text-ink mb-2 flex items-center gap-1 text-sm" >
163204 < span > ←</ span > Projects
164205 </ button >
165- < h1 className = "font-serif text-2xl font-semibold text-ink truncate" >
166- { projectPath ? formatPath ( projectPath ) : projectId }
167- </ h1 >
206+ < div className = "flex items-center justify-between gap-4" >
207+ < h1 className = "font-serif text-2xl font-semibold text-ink truncate" >
208+ { projectPath ? formatPath ( projectPath ) : projectId }
209+ </ h1 >
210+ { totalUsage . cost_usd > 0 && (
211+ < div className = "flex items-center gap-3 text-sm text-muted-foreground shrink-0" >
212+ < span title = "Total tokens (input + output)" >
213+ { formatTokens ( totalUsage . input_tokens + totalUsage . output_tokens ) } tokens
214+ </ span >
215+ < span className = "font-medium text-primary" title = "Estimated cost" >
216+ { formatCost ( totalUsage . cost_usd ) }
217+ </ span >
218+ </ div >
219+ ) }
220+ </ div >
168221 </ header >
169222
170223 < div className = "relative mb-6" >
@@ -305,6 +358,7 @@ generator: "Lovcode"
305358 < div className = "space-y-3" >
306359 { filteredSessions . map ( ( session ) => {
307360 const isSelected = selectedIds . has ( session . id ) ;
361+ const usage = usageMap . get ( session . id ) ;
308362 return (
309363 < div
310364 key = { session . id }
@@ -315,10 +369,21 @@ generator: "Lovcode"
315369 >
316370 < div className = "flex items-start justify-between gap-2" >
317371 < div className = "flex-1 min-w-0" >
318- < p className = "font-medium text-ink line-clamp-2" > { session . summary || "Untitled session" } </ p >
319- < p className = "text-sm text-muted-foreground mt-2" >
320- { session . message_count } messages · { formatDate ( session . last_modified ) }
321- </ p >
372+ < p className = "font-medium text-ink line-clamp-2" > { toReadable ( session . summary ) || "Untitled session" } </ p >
373+ < div className = "flex items-center gap-3 mt-2 text-sm text-muted-foreground" >
374+ < span > { session . message_count } messages</ span >
375+ < span > ·</ span >
376+ < span > { formatDate ( session . last_modified ) } </ span >
377+ { usage && usage . cost_usd > 0 && (
378+ < >
379+ < span > ·</ span >
380+ < span title = { `Input: ${ formatTokens ( usage . input_tokens ) } , Output: ${ formatTokens ( usage . output_tokens ) } ` } >
381+ { formatTokens ( usage . input_tokens + usage . output_tokens ) } tokens
382+ </ span >
383+ < span className = "text-primary font-medium" > { formatCost ( usage . cost_usd ) } </ span >
384+ </ >
385+ ) }
386+ </ div >
322387 </ div >
323388 { selectMode && (
324389 < input
0 commit comments