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
This commit is contained in:
AgentSeal 2026-04-15 04:46:12 -07:00
parent ea5fd90a68
commit 11cdcaa89d
2 changed files with 74 additions and 15 deletions

View file

@ -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<string, number> = {}
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 (
<Panel title="Core Tools" color={PANEL_COLORS.tools} width={pw}>
<Panel title={title ?? 'Core Tools'} color={PANEL_COLORS.tools} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)}</Text>
{sorted.slice(0, 10).map(([tool, calls]) => (
<Text key={tool} wrap="truncate-end">
@ -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 (
<Box flexDirection="column" width={dashWidth}>
<Overview projects={projects} label={PERIOD_LABELS[period]} width={dashWidth} />
<Row wide={wide} width={dashWidth}>
<DailyActivity projects={projects} days={period === 'month' || period === '30days' ? 31 : 14} pw={pw} bw={barWidth} />
<DailyActivity projects={projects} days={days} pw={pw} bw={barWidth} />
<ProjectBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
@ -489,12 +491,17 @@ function DashboardContent({ projects, period, columns }: { projects: ProjectSumm
<ModelBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<Row wide={wide} width={dashWidth}>
<ToolBreakdown projects={projects} pw={pw} bw={barWidth} />
<BashBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<McpBreakdown projects={projects} pw={dashWidth} bw={barWidth} />
{isCursor ? (
<ToolBreakdown projects={projects} pw={dashWidth} bw={barWidth} title="Languages" />
) : (
<>
<Row wide={wide} width={dashWidth}>
<ToolBreakdown projects={projects} pw={pw} bw={barWidth} />
<BashBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<McpBreakdown projects={projects} pw={dashWidth} bw={barWidth} />
</>
)}
</Box>
)
}
@ -607,7 +614,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return (
<Box flexDirection="column" width={dashWidth}>
<PeriodTabs active={period} providerName={activeProvider} showProvider={multipleProviders} />
<DashboardContent projects={projects} period={period} columns={columns} />
<DashboardContent projects={projects} period={period} columns={columns} activeProvider={activeProvider} />
<StatusBar width={dashWidth} showProvider={multipleProviders} />
</Box>
)

View file

@ -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<string>()
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<string>): { 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<string>): { 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<string, string> = {
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<SessionSource[]> {