Skip to content

Commit 810b214

Browse files
authored
Cursor: per-project breakdown by workspace (closes per-project half of #196) (#296)
Cursor's chat history showed as a single row labeled 'cursor' in the dashboard because the global state.vscdb has no workspace field on individual bubbles. The fix joins through Cursor's per-workspace storage: 1. Walk ~/Library/Application Support/Cursor/User/workspaceStorage/* 2. For each hash dir, read workspace.json -> folder URI 3. Open that dir's state.vscdb, read ItemTable['composer.composerData'] -> allComposers list 4. Build Map<composerId, folder URI> 5. emit one SessionSource per workspace plus a catch-all 'cursor' source for composers that did not register against any workspace (multi-root workspaces, no-folder-open windows, deleted workspaces with surviving global rows) The parser decodes source.path's #cursor-ws= tag, filters the parsed bubbles to the composerIds that belong to this workspace, and yields only those. The orphan-tag source negates the filter so it captures every composer not in any workspace. In passing, fix a real bug in the old code: parseBubbles set `sessionId: row.conversation_id ?? 'unknown'`, but the JSON `conversationId` field is empty in current Cursor builds, so every call shipped with `sessionId: 'unknown'`. We now derive the composer id from the row key (`bubbleId:<composerId>:<bubbleUuid>`) which is what the workspace map joins on. The old behavior masked the bug because every call went into a single 'cursor' project anyway; with per-workspace bucketing the bug becomes load-bearing. Cache version bumped 2 -> 3 to invalidate caches that still record 'unknown' as the session id. Live-tested against my real 1.9 GB Cursor DB: the single 'cursor' row with 1904 calls / $4.08 now breaks into 5 workspaces plus an orphan bucket, totals reconcile exactly. 8 fixture-based tests cover multi-workspace routing, orphan filtering, legacy bare DB path backwards compat, multi-root workspace skip, vscode-remote URI slugification, and total reconciliation across all sources. Full suite: 46 files, 653 tests passing.
1 parent 180e14d commit 810b214

5 files changed

Lines changed: 644 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@
4141
reconcile. Closes #279.
4242

4343
### Fixed (CLI)
44+
- **Cursor sessions break down by project, not one row called "cursor".**
45+
Cursor's chat history sat under a single dashboard row labeled `cursor`
46+
because the provider had no way to attribute bubbles to a workspace.
47+
The fix walks `~/Library/Application Support/Cursor/User/workspaceStorage/*`
48+
for each workspace's `workspace.json` (folder URI) and
49+
`composer.composerData` (the composer ids opened in that workspace),
50+
then joins those composer ids against the global bubbles. Each
51+
workspace becomes its own project row, sanitized into the same slug
52+
shape Claude uses (e.g. `-Users-you-myproject`); composers that have
53+
no workspace mapping (multi-root workspaces, "no folder open"
54+
sessions, deleted workspaces) remain under a catch-all `cursor` row.
55+
As part of this the cursor parser now derives `sessionId` from the
56+
bubble row key (`bubbleId:<composerId>:<bubbleUuid>`) instead of the
57+
empty `conversationId` JSON field, which was always falling back to
58+
`'unknown'`. Cursor result cache version bumped to 3 to invalidate
59+
prior caches that recorded the old session id. Closes the per-project
60+
half of #196.
4461
- **Cursor cost shown for every model, not just Auto.** Cursor emits model
4562
names in a `claude-<dot-version>-<tier>` shape (`claude-4.6-sonnet`,
4663
`claude-4.5-opus`, `claude-4.5-opus-high-thinking`, etc.) plus its own

src/cursor-cache.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { randomBytes } from 'crypto'
55

66
import type { ParsedProviderCall } from './providers/types.js'
77

8-
const CURSOR_CACHE_VERSION = 2
8+
// Bumped to 3 for the workspace-aware breakdown change: the cursor parser
9+
// now derives `sessionId` from the bubble row key (the real composer id)
10+
// rather than the empty `conversationId` JSON field, and the workspace
11+
// router relies on those composer ids to bucket calls per project.
12+
// Version 2 caches contain `sessionId: 'unknown'` for every call and would
13+
// route everything to the orphan project, so we invalidate them.
14+
const CURSOR_CACHE_VERSION = 3
915

1016
type ResultCache = {
1117
version?: number

src/daily-cache.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import { homedir } from 'os'
55
import { join } from 'path'
66
import type { DateRange, ProjectSummary } from './types.js'
77

8-
export const DAILY_CACHE_VERSION = 4
8+
// Bumped to 5 alongside the Cursor per-project breakdown: prior daily
9+
// entries recorded every Cursor session under a single 'cursor' project
10+
// label. After the upgrade, the breakdown produces per-workspace project
11+
// labels for new days; without invalidation the dashboard would show
12+
// 'cursor' for historical days and `-Users-you-myproject` for new ones
13+
// in the same window, producing a confusing mixed projection. v5 forces a
14+
// full recompute.
15+
export const DAILY_CACHE_VERSION = 5
916
const MIN_SUPPORTED_VERSION = 2
1017
const DAILY_CACHE_FILENAME = 'daily-cache.json'
1118

0 commit comments

Comments
 (0)