@@ -145,6 +145,20 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
145145 let claudeTotal = 0 ;
146146 let messageTotal = 0 ;
147147 const pathMap = new Map ( ) ;
148+ const sourceMessageTotals = { codex : 0 , claude : 0 } ;
149+ const hourCounts = Array . from ( { length : 24 } , ( _ , hour ) => ( {
150+ key : String ( hour ) . padStart ( 2 , '0' ) ,
151+ label : String ( hour ) . padStart ( 2 , '0' ) ,
152+ count : 0
153+ } ) ) ;
154+ const weekdayLabels = [ '周一' , '周二' , '周三' , '周四' , '周五' , '周六' , '周日' ] ;
155+ const weekdayCounts = Array . from ( { length : 7 } , ( _ , index ) => ( {
156+ key : String ( index ) ,
157+ label : weekdayLabels [ index ] ,
158+ count : 0
159+ } ) ) ;
160+ const recentSessions = [ ] ;
161+ const topSessionsByMessages = [ ] ;
148162
149163 for ( const session of list ) {
150164 if ( ! session || typeof session !== 'object' ) continue ;
@@ -169,10 +183,44 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
169183 claudeTotal += 1 ;
170184 }
171185 messageTotal += messageCount ;
186+ sourceMessageTotals [ source ] += messageCount ;
187+
188+ const utcHour = stamp . getUTCHours ( ) ;
189+ if ( hourCounts [ utcHour ] ) {
190+ hourCounts [ utcHour ] . count += 1 ;
191+ }
192+ const dayIndex = ( stamp . getUTCDay ( ) + 6 ) % 7 ;
193+ if ( weekdayCounts [ dayIndex ] ) {
194+ weekdayCounts [ dayIndex ] . count += 1 ;
195+ }
196+
172197 const cwd = normalizeSessionPathFilter ( session . cwd ) ;
173198 if ( cwd ) {
174- pathMap . set ( cwd , ( Number ( pathMap . get ( cwd ) ) || 0 ) + 1 ) ;
199+ const prev = pathMap . get ( cwd ) || { count : 0 , messageTotal : 0 , updatedAtMs : 0 } ;
200+ pathMap . set ( cwd , {
201+ count : prev . count + 1 ,
202+ messageTotal : prev . messageTotal + messageCount ,
203+ updatedAtMs : Math . max ( prev . updatedAtMs , updatedAtMs )
204+ } ) ;
175205 }
206+
207+ const normalizedTitle = typeof session . title === 'string' && session . title . trim ( )
208+ ? session . title . trim ( )
209+ : ( typeof session . sessionId === 'string' && session . sessionId . trim ( ) ? session . sessionId . trim ( ) : '未命名会话' ) ;
210+ const sessionEntry = {
211+ key : `${ source } :${ session . sessionId || '' } :${ session . filePath || normalizedTitle } ` ,
212+ title : normalizedTitle ,
213+ source,
214+ sourceLabel : source === 'codex' ? 'Codex' : 'Claude Code' ,
215+ cwd,
216+ messageCount,
217+ updatedAt : session . updatedAt || '' ,
218+ updatedAtMs,
219+ updatedAtLabel : formatSessionTimelineTimestamp ( session . updatedAt || '' ) ,
220+ hasExactMessageCount : session . __messageCountExact === true
221+ } ;
222+ recentSessions . push ( sessionEntry ) ;
223+ topSessionsByMessages . push ( sessionEntry ) ;
176224 }
177225
178226 const totalSessions = codexTotal + claudeTotal ;
@@ -181,16 +229,41 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
181229 { key : 'claude' , label : 'Claude' , value : claudeTotal }
182230 ] . map ( ( item ) => ( {
183231 ...item ,
184- percent : totalSessions > 0 ? Math . round ( ( item . value / totalSessions ) * 100 ) : 0
232+ percent : totalSessions > 0 ? Math . round ( ( item . value / totalSessions ) * 100 ) : 0 ,
233+ messageTotal : sourceMessageTotals [ item . key ] || 0 ,
234+ messagePercent : messageTotal > 0 ? Math . round ( ( ( sourceMessageTotals [ item . key ] || 0 ) / messageTotal ) * 100 ) : 0 ,
235+ avgMessages : item . value > 0 ? Math . round ( ( ( sourceMessageTotals [ item . key ] || 0 ) / item . value ) * 10 ) / 10 : 0
185236 } ) ) ;
186237
187238 const topPaths = [ ...pathMap . entries ( ) ]
188- . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] || a [ 0 ] . localeCompare ( b [ 0 ] , 'zh-Hans-CN' ) )
239+ . sort ( ( a , b ) => b [ 1 ] . count - a [ 1 ] . count || b [ 1 ] . messageTotal - a [ 1 ] . messageTotal || a [ 0 ] . localeCompare ( b [ 0 ] , 'zh-Hans-CN' ) )
189240 . slice ( 0 , 5 )
190- . map ( ( [ pathValue , count ] ) => ( { path : pathValue , count } ) ) ;
241+ . map ( ( [ pathValue , meta ] ) => ( {
242+ path : pathValue ,
243+ count : meta . count ,
244+ messageTotal : meta . messageTotal ,
245+ updatedAtLabel : meta . updatedAtMs ? formatSessionTimelineTimestamp ( new Date ( meta . updatedAtMs ) . toISOString ( ) ) : ''
246+ } ) ) ;
247+
248+ const sortedRecentSessions = recentSessions
249+ . sort ( ( a , b ) => b . updatedAtMs - a . updatedAtMs || b . messageCount - a . messageCount || a . title . localeCompare ( b . title , 'zh-Hans-CN' ) )
250+ . slice ( 0 , 6 ) ;
251+
252+ const sortedTopSessionsByMessages = topSessionsByMessages
253+ . sort ( ( a , b ) => b . messageCount - a . messageCount || b . updatedAtMs - a . updatedAtMs || a . title . localeCompare ( b . title , 'zh-Hans-CN' ) )
254+ . slice ( 0 , 6 ) ;
191255
192256 const maxSessionBucket = buckets . reduce ( ( max , item ) => Math . max ( max , item . totalSessions ) , 0 ) ;
193257 const maxMessageBucket = buckets . reduce ( ( max , item ) => Math . max ( max , item . totalMessages ) , 0 ) ;
258+ const maxHourCount = hourCounts . reduce ( ( max , item ) => Math . max ( max , item . count ) , 0 ) ;
259+ const maxWeekdayCount = weekdayCounts . reduce ( ( max , item ) => Math . max ( max , item . count ) , 0 ) ;
260+ const busiestDay = [ ...buckets ]
261+ . sort ( ( a , b ) => b . totalSessions - a . totalSessions || b . totalMessages - a . totalMessages || a . key . localeCompare ( b . key , 'zh-Hans-CN' ) ) [ 0 ] || null ;
262+ const busiestHour = [ ...hourCounts ]
263+ . sort ( ( a , b ) => b . count - a . count || a . key . localeCompare ( b . key , 'zh-Hans-CN' ) ) [ 0 ] || null ;
264+ const activeDays = buckets . filter ( ( item ) => item . totalSessions > 0 ) . length ;
265+ const avgMessagesPerSession = totalSessions > 0 ? Math . round ( ( messageTotal / totalSessions ) * 10 ) / 10 : 0 ;
266+ const avgSessionsPerActiveDay = activeDays > 0 ? Math . round ( ( totalSessions / activeDays ) * 10 ) / 10 : 0 ;
194267
195268 return {
196269 range,
@@ -200,12 +273,41 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
200273 totalMessages : messageTotal ,
201274 codexTotal,
202275 claudeTotal,
203- activeDays : buckets . filter ( ( item ) => item . totalSessions > 0 ) . length
276+ activeDays,
277+ avgMessagesPerSession,
278+ avgSessionsPerActiveDay,
279+ busiestDay : busiestDay
280+ ? {
281+ key : busiestDay . key ,
282+ label : busiestDay . label ,
283+ totalSessions : busiestDay . totalSessions ,
284+ totalMessages : busiestDay . totalMessages
285+ }
286+ : null ,
287+ busiestHour : busiestHour
288+ ? {
289+ key : busiestHour . key ,
290+ label : `${ busiestHour . label } :00` ,
291+ count : busiestHour . count
292+ }
293+ : null
204294 } ,
205295 sourceShare,
206296 topPaths,
297+ recentSessions : sortedRecentSessions ,
298+ topSessionsByMessages : sortedTopSessionsByMessages ,
299+ hourActivity : hourCounts . map ( ( item ) => ( {
300+ ...item ,
301+ percent : maxHourCount > 0 ? Math . round ( ( item . count / maxHourCount ) * 100 ) : 0
302+ } ) ) ,
303+ weekdayActivity : weekdayCounts . map ( ( item ) => ( {
304+ ...item ,
305+ percent : maxWeekdayCount > 0 ? Math . round ( ( item . count / maxWeekdayCount ) * 100 ) : 0
306+ } ) ) ,
207307 maxSessionBucket,
208- maxMessageBucket
308+ maxMessageBucket,
309+ maxHourCount,
310+ maxWeekdayCount
209311 } ;
210312}
211313
0 commit comments