Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,7 @@ async function scanProjectDirs(
const session = buildSessionSummary(sessionId, projectName, classifiedTurns, mcpInv, source)
session.agentType = cachedFile.agentType
if (cachedFile.prLinks?.length) session.prLinks = [...new Set(cachedFile.prLinks)].sort()
if (cachedFile.title) session.title = cachedFile.title

if (session.apiCalls > 0) {
const projectKey = cachedFile.canonicalCwd
Expand Down Expand Up @@ -2744,7 +2745,7 @@ async function parseProviderSources(

// Query-time: derive SessionSummary from all cached turns.
// Uses seenKeys (shared across providers) for cross-provider dedup.
const sessionMap = new Map<string, { project: string; projectPath?: string; turns: ClassifiedTurn[]; prLinks?: Set<string> }>()
const sessionMap = new Map<string, { project: string; projectPath?: string; turns: ClassifiedTurn[]; prLinks?: Set<string>; title?: string }>()

for (const source of servedSources) {
const cachedFile = section.files[source.path]
Expand Down Expand Up @@ -2777,12 +2778,14 @@ async function parseProviderSources(
const links = (existing.prLinks ??= new Set())
for (const link of cachedFile.prLinks) links.add(link)
}
if (!existing.title && cachedFile.title) existing.title = cachedFile.title
} else {
sessionMap.set(key, {
project,
projectPath: turn.calls[0]?.projectPath,
turns: [classified],
...(cachedFile.prLinks?.length ? { prLinks: new Set(cachedFile.prLinks) } : {}),
...(cachedFile.title ? { title: cachedFile.title } : {}),
})
}
}
Expand Down Expand Up @@ -2826,10 +2829,11 @@ async function parseProviderSources(
}

const projectMap = new Map<string, { projectPath?: string; sessions: SessionSummary[] }>()
for (const [key, { project, projectPath, turns, prLinks }] of sessionMap) {
for (const [key, { project, projectPath, turns, prLinks, title }] of sessionMap) {
const sessionId = key.split(':')[1] ?? key
const session = buildSessionSummary(sessionId, project, turns)
if (prLinks?.size) session.prLinks = [...prLinks].sort()
if (title) session.title = title
if (session.apiCalls > 0) {
const existing = projectMap.get(project)
if (existing) {
Expand Down
6 changes: 5 additions & 1 deletion src/sessions-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { ProjectSummary, SessionSummary } from './types.js'

export type SessionRow = {
sessionId: string
/// Captured human title, empty when the transcript never produced one.
title: string
project: string
provider: string
models: string[]
Expand Down Expand Up @@ -41,6 +43,7 @@ function durationMs(startedAt: string, endedAt: string): number {
export function aggregateSessions(projects: ProjectSummary[]): SessionRow[] {
return projects.flatMap(project => project.sessions.map(session => ({
sessionId: session.sessionId,
title: session.title ?? '',
project: session.project || project.project,
provider: inferProvider(session),
models: Object.keys(session.modelBreakdown),
Expand All @@ -63,9 +66,10 @@ export function renderJson(rows: SessionRow[]): string {
}

export function renderTable(rows: SessionRow[]): string {
const headers = ['SESSION', 'PROJECT', 'PROVIDER', 'MODELS', 'COST', 'SAVED', 'CALLS', 'TURNS', 'STARTED']
const headers = ['SESSION', 'TITLE', 'PROJECT', 'PROVIDER', 'MODELS', 'COST', 'SAVED', 'CALLS', 'TURNS', 'STARTED']
const values = rows.map(row => [
row.sessionId,
row.title.length > 38 ? row.title.slice(0, 37) + '\u2026' : row.title,
row.project,
row.provider,
row.models.join(', '),
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ export type SessionSummary = {
/// GitHub PR URLs captured from the session transcript (session-level,
/// deduplicated). Absent when none were observed.
prLinks?: string[]
/// Human session title captured from the transcript (last ai-title entry).
/// Absent when the transcript never produced one.
title?: string
modelBreakdown: Record<string, { calls: number; costUSD: number; tokens: TokenUsage; savingsUSD: number; estimatedCostUSD?: number }>
toolBreakdown: Record<string, { calls: number }>
mcpBreakdown: Record<string, { calls: number }>
Expand Down
1 change: 1 addition & 0 deletions tests/sessions-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('sessions JSON emitter', () => {

expect(parsed).toEqual([{
sessionId: 'session-1',
title: '',
project: 'codeburn',
provider: 'claude',
models: ['claude-sonnet-4-5'],
Expand Down