diff --git a/src/parser.ts b/src/parser.ts index 50fa648..d49697b 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -550,7 +550,7 @@ async function parseProviderSources( const provider = await getProvider(providerName) if (!provider) return [] - const sessionMap = new Map() + const sessionMap = new Map() try { for (const source of sources) { @@ -574,13 +574,15 @@ async function parseProviderSources( const turn = providerCallToTurn(call) const classified = classifyTurn(turn) - const key = `${providerName}:${call.sessionId}:${source.project}` + const project = call.project ?? source.project + const key = `${providerName}:${call.sessionId}:${project}` const existing = sessionMap.get(key) if (existing) { existing.turns.push(classified) + if (!existing.projectPath && call.projectPath) existing.projectPath = call.projectPath } else { - sessionMap.set(key, { project: source.project, turns: [classified] }) + sessionMap.set(key, { project, projectPath: call.projectPath, turns: [classified] }) } } } @@ -592,22 +594,26 @@ async function parseProviderSources( } } - const projectMap = new Map() - for (const [key, { project, turns }] of sessionMap) { + const projectMap = new Map() + for (const [key, { project, projectPath, turns }] of sessionMap) { const sessionId = key.split(':')[1] ?? key const session = buildSessionSummary(sessionId, project, turns) if (session.apiCalls > 0) { - const existing = projectMap.get(project) ?? [] - existing.push(session) - projectMap.set(project, existing) + const existing = projectMap.get(project) + if (existing) { + existing.sessions.push(session) + if (!existing.projectPath && projectPath) existing.projectPath = projectPath + } else { + projectMap.set(project, { projectPath, sessions: [session] }) + } } } const projects: ProjectSummary[] = [] - for (const [dirName, sessions] of projectMap) { + for (const [dirName, { projectPath, sessions }] of projectMap) { projects.push({ project: dirName, - projectPath: unsanitizePath(dirName), + projectPath: projectPath ?? unsanitizePath(dirName), sessions, totalCostUSD: sessions.reduce((s, sess) => s + sess.totalCostUSD, 0), totalApiCalls: sessions.reduce((s, sess) => s + sess.apiCalls, 0), diff --git a/src/providers/types.ts b/src/providers/types.ts index 4e9a98a..90d5e1c 100644 --- a/src/providers/types.ts +++ b/src/providers/types.ts @@ -27,6 +27,8 @@ export type ParsedProviderCall = { deduplicationKey: string userMessage: string sessionId: string + project?: string + projectPath?: string } export type Provider = { diff --git a/src/providers/vscode-cline-parser.ts b/src/providers/vscode-cline-parser.ts index 7e1a606..ffad939 100644 --- a/src/providers/vscode-cline-parser.ts +++ b/src/providers/vscode-cline-parser.ts @@ -67,25 +67,40 @@ async function discoverClineTasksInBaseDir(baseDir: string, providerName: string } const MODEL_TAG_RE = /([^<]+)<\/model>/ +const WORKSPACE_DIR_RE = /Current Workspace Directory \(([^)]+)\)/ -function extractModelFromHistory(taskDir: string, fallbackModel: string): Promise { +type HistoryMeta = { model: string; workspace: string | null } + +function extractHistoryMeta(taskDir: string, fallbackModel: string): Promise { return readFile(join(taskDir, 'api_conversation_history.json'), 'utf-8') .then(raw => { const msgs = JSON.parse(raw) as Array<{ role?: string; content?: Array<{ text?: string }> }> - if (!Array.isArray(msgs)) return fallbackModel + if (!Array.isArray(msgs)) return { model: fallbackModel, workspace: null } + let model: string | null = null + let workspace: string | null = null for (const msg of msgs) { if (msg.role !== 'user' || !Array.isArray(msg.content)) continue for (const block of msg.content) { - const match = typeof block.text === 'string' && MODEL_TAG_RE.exec(block.text) - if (match) { - const raw = match[1] - return raw.includes('/') ? raw.split('/').pop()! : raw + if (typeof block.text !== 'string') continue + if (!model) { + const mm = MODEL_TAG_RE.exec(block.text) + if (mm) model = mm[1].includes('/') ? mm[1].split('/').pop()! : mm[1] } + if (!workspace) { + const wm = WORKSPACE_DIR_RE.exec(block.text) + if (wm) workspace = wm[1] + } + if (model && workspace) break } + if (model && workspace) break } - return fallbackModel + return { model: model ?? fallbackModel, workspace } }) - .catch(() => fallbackModel) + .catch(() => ({ model: fallbackModel, workspace: null })) +} + +function workspaceToProject(workspace: string): string { + return basename(workspace) || workspace } export function createClineParser(source: SessionSource, seenKeys: Set, providerName: string, fallbackModel = 'cline-auto'): SessionParser { @@ -110,7 +125,10 @@ export function createClineParser(source: SessionSource, seenKeys: Set, if (!Array.isArray(uiMessages)) return - const model = await extractModelFromHistory(taskDir, fallbackModel) + const meta = await extractHistoryMeta(taskDir, fallbackModel) + const model = meta.model + const project = meta.workspace ? workspaceToProject(meta.workspace) : undefined + const projectPath = meta.workspace ?? undefined let userMessage = '' for (const msg of uiMessages) { @@ -173,6 +191,8 @@ export function createClineParser(source: SessionSource, seenKeys: Set, deduplicationKey: dedupKey, userMessage: index === 0 ? userMessage : '', sessionId: taskId, + project, + projectPath, } } },