From 11cdcaa89ddbc750db01e92edd78fbe8ee2e6714 Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Wed, 15 Apr 2026 04:46:12 -0700 Subject: [PATCH] feat: activity classification + language breakdown for Cursor - Extract user text from bubbles for activity classifier - Extract codeBlocks languageId for programming language breakdown - Show Languages panel instead of Core Tools/Shell/MCP for Cursor - Adaptive dashboard layout based on active provider - 120-day daily activity range for longer periods --- src/dashboard.tsx | 29 ++++++++++++-------- src/providers/cursor.ts | 60 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 8561e01..10879b6 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -304,7 +304,7 @@ function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; p ) } -function ToolBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { +function ToolBreakdown({ projects, pw, bw, title }: { projects: ProjectSummary[]; pw: number; bw: number; title?: string }) { const toolTotals: Record = {} for (const project of projects) { for (const session of project.sessions) { @@ -318,7 +318,7 @@ function ToolBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: n const nw = Math.max(6, pw - bw - 15) return ( - + {''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)} {sorted.slice(0, 10).map(([tool, calls]) => ( @@ -462,8 +462,9 @@ function Row({ wide, width, children }: { wide: boolean; width: number; children return <>{children} } -function DashboardContent({ projects, period, columns }: { projects: ProjectSummary[]; period: Period; columns?: number }) { +function DashboardContent({ projects, period, columns, activeProvider }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string }) { const { dashWidth, wide, halfWidth, barWidth } = getLayout(columns) + const isCursor = activeProvider === 'cursor' if (projects.length === 0) { return ( @@ -474,13 +475,14 @@ function DashboardContent({ projects, period, columns }: { projects: ProjectSumm } const pw = wide ? halfWidth : dashWidth + const days = period === 'month' || period === '30days' ? 31 : period === '120days' ? 120 : 14 return ( - + @@ -489,12 +491,17 @@ function DashboardContent({ projects, period, columns }: { projects: ProjectSumm - - - - - - + {isCursor ? ( + + ) : ( + <> + + + + + + + )} ) } @@ -607,7 +614,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, return ( - + ) diff --git a/src/providers/cursor.ts b/src/providers/cursor.ts index 6d28505..0bec236 100644 --- a/src/providers/cursor.ts +++ b/src/providers/cursor.ts @@ -29,6 +29,8 @@ type BubbleRow = { model: string | null created_at: string | null conversation_id: string | null + user_text: string | null + code_blocks: string | null } function getCursorDbPath(): string { @@ -41,6 +43,25 @@ function getCursorDbPath(): string { return join(homedir(), '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb') } +type CodeBlock = { languageId?: string } + +function extractLanguages(codeBlocksJson: string | null): string[] { + if (!codeBlocksJson) return [] + try { + const blocks = JSON.parse(codeBlocksJson) as CodeBlock[] + if (!Array.isArray(blocks)) return [] + const langs = new Set() + for (const block of blocks) { + if (block.languageId && block.languageId !== 'plaintext') { + langs.add(block.languageId) + } + } + return [...langs] + } catch { + return [] + } +} + function resolveModel(raw: string | null): string { if (!raw || raw === 'default') return CURSOR_DEFAULT_MODEL return raw @@ -57,7 +78,9 @@ const BUBBLE_QUERY_BASE = ` json_extract(value, '$.tokenCount.outputTokens') as output_tokens, json_extract(value, '$.modelInfo.modelName') as model, json_extract(value, '$.createdAt') as created_at, - json_extract(value, '$.conversationId') as conversation_id + json_extract(value, '$.conversationId') as conversation_id, + substr(json_extract(value, '$.text'), 1, 500) as user_text, + json_extract(value, '$.codeBlocks') as code_blocks FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND json_extract(value, '$.tokenCount.inputTokens') > 0 @@ -112,6 +135,9 @@ function parseBubbles(db: SqliteDatabase, seenKeys: Set): { calls: Parse const costUSD = calculateCost(pricingModel, inputTokens, outputTokens, 0, 0, 0) const timestamp = createdAt || '' + const userText = row.user_text ?? '' + + const languages = extractLanguages(row.code_blocks) results.push({ provider: 'cursor', @@ -124,11 +150,11 @@ function parseBubbles(db: SqliteDatabase, seenKeys: Set): { calls: Parse reasoningTokens: 0, webSearchRequests: 0, costUSD, - tools: [], + tools: languages, timestamp, speed: 'standard', deduplicationKey: dedupKey, - userMessage: '', + userMessage: userText, sessionId: conversationId, }) } catch { @@ -189,7 +215,33 @@ export function createCursorProvider(dbPathOverride?: string): Provider { }, toolDisplayName(rawTool: string): string { - return rawTool + const langNames: Record = { + javascript: 'JavaScript', + typescript: 'TypeScript', + python: 'Python', + rust: 'Rust', + go: 'Go', + java: 'Java', + cpp: 'C++', + c: 'C', + csharp: 'C#', + ruby: 'Ruby', + php: 'PHP', + swift: 'Swift', + kotlin: 'Kotlin', + html: 'HTML', + css: 'CSS', + scss: 'SCSS', + json: 'JSON', + yaml: 'YAML', + markdown: 'Markdown', + sql: 'SQL', + shell: 'Shell', + bash: 'Bash', + dockerfile: 'Dockerfile', + toml: 'TOML', + } + return langNames[rawTool] ?? rawTool }, async discoverSessions(): Promise {