Skip to content

par4d15e/bettercursor

Repository files navigation

bettercursor

Local Cursor session viewer (read-only). Tauri 2 + React 19 + Rust, UI inspired by cc-switch.

🌐 English · 简体中文

app release platform stack language i18n sync

Current repo tag: v0.3.7d. The embedded app version fields in package.json / Cargo.toml / tauri.conf.json are 0.3.7, so the v0.3.7c / v0.3.7d maintenance line builds artifacts on the current 0.3.7 app version instead of the old 0.3.6 package line.

What it is

bettercursor is a desktop app that views every AI conversation Cursor IDE stores on disk. It scans the three SQLite + JSONL layers under ~/.config/Cursor (Linux) / ~/Library/Application Support/Cursor (macOS), deduplicates across layers, and renders a single merged session list.

Design goals:

  • Read-only by default — v0.2.1+ added opt-in writes (fix_orphans / delete_session / sync_session_layer23) but the app never touches Cursor's working files except through these explicit commands
  • cc-switch UI — left project-grouped tree + right conversation detail
  • Byte-identical to a Python reference implementation — MD5 chat_root parity tests pass

Feature status

v0.3.7d (✅ latest maintenance tag, 2026-07-07)

  • Self-discovery filter — nearby mDNS browse now hides the local machine itself instead of offering a bogus self-pair target
  • Trusted-peer cleanup — legacy trusted_peers.json entries with wrong/self device names are repaired on load and reconciled against fresh discovery results when available
  • Stable pairing identity — LAN pairing now prefers a PAIR2 handshake that exchanges stable device_id + real host name and falls back to legacy PAIR for backward compatibility

v0.3.7c (2026-07-07)

  • Version-line alignmentpackage.json / Cargo.toml / tauri.conf.json now all report 0.3.7, so packaged app metadata finally matches the v0.3.7* release line
  • Release-doc cleanup — README / roadmap / download notes now distinguish historical v0.3.7a / v0.3.7b artifacts from the new 0.3.7 package line

v0.3.7b (✅ latest maintenance tag, 2026-07-06)

  • LAN discovery fix — mDNS publish now uses ServiceInfo::enable_addr_auto() so peers advertise reachable local addresses instead of empty address sets
  • Browse lifecycle fixdiscovery_browse now consumes ServiceResolved events in-process and explicitly stop_browse / shutdowns the daemon
  • Reachable host preference — discovered peers prefer non-loopback IPv4, then IPv6, avoiding 127.0.0.1 false positives and malformed host:port

v0.3.7a (2026-07-06)

  • Interaction latency cut-down — heavy sync_now / conversation parse / write-path work moved off the UI thread
  • Single authoritative refresh — write actions emit one backend refresh; removed extra frontend sync_now loops
  • Watcher self-write suppression — fs watcher ignores a short self-write window to avoid duplicate full rescans
  • Settings-load unblock — LAN browse no longer runs eagerly on settings open; nearby peer scan is manual

v0.3.7 (⚪ planned next, 2026-07-07 scoped)

  • LAN-only pairing flow refresh — promote manual host:port + code pairing to the primary path for virtual LANs such as Tailscale / ZeroTier / Headscale
  • Local address picker — choose which local interface/address to advertise and display in settings (physical LAN + virtual overlays)
  • Stable listen port — keep auto-assign by default, but allow an advanced fixed port for firewall / overlay reuse
  • mDNS as convenience, not prerequisite — nearby scan stays available on multicast-friendly networks, but failure to discover peers no longer blocks the sync flow
  • SSH/T2b path removed from active roadmap — no new SSH peer UI; the v0.2.6 SSH config remains historical/reference only

v0.3.6 (shipped 2026-07-05)

  • v4 snapshot enrichmentimages + blob_refs / raw_blobs (agentKv); restored on apply
  • Mac↔Linux path rewritepath_rewrite + optional path_mappings in ~/.bettercursor/config.json
  • Identical → apply missing L2/L3 — when unified.db matches but Cursor layers are absent locally
  • Pull result UIPullReport.results + per-session status in <SyncPeersPanel>
  • Background auto-pull — periodic pull from trusted LAN peers; toggle + interval in settings

v0.3.5 (2026-07-05)

  • Optional L3 soft delete — Desktop-aligned sidebar archive + purge bubbleId / checkpointId rows; keep composerData shell
  • Subagent sessions — read L2 meta[0].subagentInfo; nest under rootParentAgentId in the sidebar tree; collapsed by default
  • Hide empty Desktop ghostsUntitled · uuid, zero bubbles, no CLI source: filtered out of the session list (disk untouched)
  • Conversation read fixes — L3 header chain + L2 enrichment; trim bad L3 prefixes; strip context envelopes / [REDACTED]
  • Delete tombstonesdeleted_sessions stops L3-only rows from resurrecting after bettercursor delete

v0.3.4 (2026-07-05)

  • L2→L3 bubble enrichmentlayer2_messages walks the CLI store.db DAG and replaces L1 [REDACTED] stubs with full assistant text + tool-call metadata before bubbleId inject
  • User image attachments — L2 image blobs decode to images[] data URLs on user bubbleId rows (Desktop replay)
  • Re-inject detection — sessions with CLI envelopes, redacted assistant text, or missing images trigger Layer 3 rewrite
  • 补 Layer 3 playbook — see SYNC_DESIGN §0.5 (quit Cursor → sync in bettercursor → restart Cursor)

v0.3.2 (2026-07-05)

  • <SettingsDialog> — gear icon in the sidebar header; consolidates language switch, cross-device sync (<SyncPeersPanel>), and conflict resolution (<ConflictResolvePanel>)
  • i18n fix — merged duplicate sync keys in locale JSON (status badge no longer shows raw sync.autoSync)
  • Dark-theme language switcher — segmented buttons replace native <select>; color-scheme: dark on root
  • Sidebar polish — product name BetterCursor in header; collapse/expand all project groups; removed non-functional back button
  • Conflict UX — neutral copy + badge on settings when conflicts pending

v0.3.1 (2026-07-05)

  • LAN cross-device sync — mDNS discovery, 6-digit pairing, trusted peers, outbox, background sync loop
  • <SyncPeersDialog> / <ConflictResolveDialog> — shipped in v0.3.1; superseded by <SettingsDialog> in v0.3.2

v0.3.0 (2026-07-05)

  • ~/.bettercursor/unified.db (PR-1): 8 tables + FTS5 + rebuild_from_cursor_state + archive / conflicts / sync_runs
  • pre-PR-2 read-path parity: full L3 bubble text extraction, Cursor 3.0+ session discovery, timestamp gap fill, cursor-history parity fixtures
  • snapshot codec v4 (core/snapshot.rs): bubble-level JSON; push still uses 8-field snapshot_meta
  • Conflict 5-way (core/conflict.rs): classify / bubble_diff / auto_merge; transport_pull writes back into unified.db
  • Transport async (tokio + async-trait); Tauri commands stay sync, backend uses block_on
  • agentKv minimal slice: write_layer3 copies agent blobs referenced by conversationState
  • 126 Rust unit tests (cargo test --lib)

Inspect unified.db:

sqlite3 ~/.bettercursor/unified.db "SELECT uuid, bubble_count, content_hash FROM sessions LIMIT 5;"

v0.2.6 (shipped 2026-07-04)

  • Cross-device sync — Transport trait first cut: core::transport::Transport trait (4 methods: push / pull / list_remote / endpoint_id, sync — deliberately diverging from the async_trait in SYNC_DESIGN §4.4 until v0.3.0). One impl: SshRsyncTransport (T2), shelling out to system ssh / rsync (no new Cargo deps, no tokio, no russh).
  • Minimum v0.2.6 snapshot carrier: SessionSnapshot (8 metadata fields — uuid / last_updated_at_ms / host / project_slug / project_path / source_path / text_preview capped 280 chars / bubble_count). No bubbles / blobs yet — that's v0.3.0 unified.db territory.
  • Peer config file: ~/.bettercursor/transports.json (separate from the prefs config.json). Atomic save (*.tmp + rename).
  • 4 Tauri commands: transport_list_peers / transport_test / transport_push / transport_pull. Plus 4 typed IPC wrappers in src/lib/tauri.ts (PeerSummary / TestReport / PushReport / PullReport / RemoteSession).
  • No UI yet — usage is via invoke('transport_*') from dev console + manually editing transports.json. SyncPeersDialog is a v0.3.0 milestone.
  • 20 Rust unit tests for snapshot codec, config serde, ssh_cmd safety flags, push failure stderr, etc. Plus tests/fixtures/fake-{ssh,rsync}.sh mock binaries for CI-friendly testing without a real SSH peer.
  • v0.2.6 housekeeping (shipped together): CI matrix gains macos-13 (Intel x64 dmg alongside Apple Silicon dmg), Node 20 → 22, vitest 2 + jsdom 25 + @testing-library/react 16 + 15-case test suite for <SyncStatusBadge> / <BrokenBadge> i18n-aware fallback. Zero business-code change.

v0.2.5 (shipped 2026-07-04)

  • Cross-platform packaging: Linux .deb / .AppImage + macOS unsigned .dmg (macOS 10.15+, Apple Silicon) + Windows .msi / .exe (NSIS). All built by GitHub Actions matrix (release.yml)
  • i18n (zh-CN / en): react-i18next + src/locales/{zh-CN,en}.json (~110 UI strings) + <LanguageSwitcher> header <select> + localStorage persistence (i18nextLng)
  • Three-file version bump: package.json / Cargo.toml / tauri.conf.json all on 0.2.5; productName: "BetterCursor" (PascalCase for Mac .app)
  • Background sync loop wrap-up (v0.2.3): <SyncNowButton> (instant rescan) + <SyncStatusBadge> ("● Auto-sync · 12s ago", 1Hz tick + 5s backend poll)
  • Conversation record expansion (v0.2.2): L1+L2+L3 three-way merge + <MessageList> thin wrapper
  • Repair orphan + delete session (v0.2.1): native <dialog> confirm
  • Scans 3 Cursor layers on startup (Layer 1 JSONL / Layer 2 store.db / Layer 3 state.vscdb)
  • Cross-layer dedup; project grouping; full-text search by name / project / content / UUID
  • MD5 chat_root byte-identical to the Python reference

v0.3.8+ (planned, see SYNC_DESIGN.md)

  • T3/T4/T5 — Git / S3 / specialized overlay discovery adapters (TBD)
  • PR-2b Doctor — deferred; observe orphan cases first (SYNC_DESIGN §10.4.3)

Historical SSH cross-device (v0.2.6–v0.3.6 reference only)

Before the v0.3.7 LAN-only plan, advanced users could configure SSH/rsync peers in ~/.bettercursor/transports.json:

{
  "peers": [
    {
      "id": "linux-desktop",
      "kind": "ssh",
      "host": "[email protected]",
      "port": 22,
      "identity_file": "~/.ssh/id_ed25519",
      "remote_snap_dir": "~/.bettercursor/peers/bettercursor-main",
      "remote_hostname": "linux-desktop"
    }
  ]
}

This path is now historical/reference only. The active roadmap favors LAN manual-address pairing over an SSH settings UI.

Download & install

Every git tag (v*.*.*) triggers .github/workflows/release.yml to build on three platforms. Artifacts end up on the Releases page.

Current repo config is already on the 0.3.7 app version line. Older artifacts built from v0.3.7a / v0.3.7b may still carry 0.3.6 filenames because those tags were cut before the version bump landed.

Linux

# Debian / Ubuntu (.deb — declares libwebkit2gtk-4.1 / libgtk-3 /
# libayatana-appindicator3 in Depends:)
sudo dpkg -i BetterCursor_0.2.6_amd64.deb
sudo apt-get install -f   # satisfy missing deps if dpkg complains

# Portable AppImage (no install, but first run downloads linuxdeploy
# binaries from tauri-apps/binary-releases — needs network)
chmod +x BetterCursor_0.2.6_amd64.AppImage
./BetterCursor_0.2.6_amd64.AppImage

macOS

  1. Download BetterCursor_0.2.6_aarch64.dmg (Apple Silicon) or BetterCursor_0.2.6_x64.dmg (Intel). Both are unsigned .dmg built by the macos-latest + macos-13 CI matrix entries.

  2. Mount, drag BetterCursor.app into /Applications.

  3. Bypass Gatekeeper for an unsigned app (one-shot, cleaner than right-click → Open):

    xattr -dr com.apple.quarantine /Applications/BetterCursor.app

    com.apple.quarantine is the extended attribute Finder drops on anything downloaded from the internet. -dr recurses through the whole .app bundle (including nested binaries and frameworks) and strips every quarantine flag, so subsequent double-clicks work like an App Store install.

    Fallback (if -dr doesn't stick): right-click BetterCursor.app → Open → Open. Same effect, but you'll have to repeat for every new build.

    Sweep everything in /Applications in one go:

    find /Applications -name "*.app" -exec xattr -dr com.apple.quarantine {} \; 2>/dev/null

Windows

# .msi (MSI installer — good for managed deployment)
msiexec /i BetterCursor_0.2.6_x64_en-US.msi

# or .exe (NSIS — better for personal installs)
.\BetterCursor_0.2.6_x64-setup.exe

Quick start (from source)

Prerequisites

  • Node 20+ + pnpm 9+ (lockfile is lockfileVersion: '9.0')
  • Rust 1.77+ (rustup install stable)
  • Linux: webkit2gtk-4.1, libsoup-3.0, libgtk-3, libjavascriptcoregtk-4.1, optional xdg-desktop-portal-gnome
  • macOS: Xcode Command Line Tools
  • Windows: WebView2 runtime (preinstalled on Win 11)

Install & run

git clone https://github.com/par4d15e/bettercursor.git
cd bettercursor
pnpm install

# Dev (HMR + WebKit devtools available)
pnpm tauri dev

# Production build
pnpm tauri build

On launch a 1280×800 window opens; in the background the app scans Cursor's storage and renders ~37 sessions (Linux desktop + Linux CLI

  • macOS sources mix, in a typical setup).

Wayland note

Some compositors need fallback env vars under WebKitGTK:

WEBKIT_DISABLE_DMABUF_RENDERER=1 \
LIBGL_ALWAYS_SOFTWARE=1 \
pnpm tauri dev

Project layout

bettercursor/
├── src/                   # React + TS frontend
│   ├── components/        # SessionTree, SessionDetail, SourceBadge, ...
│   ├── store/             # Zustand store + selectors
│   ├── lib/               # tauri.ts (IPC wrapper), types.ts
│   ├── i18n/              # i18next init (zh-CN, en)
│   ├── locales/           # zh-CN.json / en.json
│   ├── App.tsx · main.tsx · index.css
├── src-tauri/             # Rust backend
│   ├── src/
│   │   ├── core/
│   │   │   ├── paths.rs       # 4-layer path resolution
│   │   │   ├── storage.rs     # WAL-safe SQLite reader
│   │   │   ├── canonical.rs   # three-layer merge
│   │   ├── lib.rs             # Tauri commands + setup
│   │   ├── main.rs
│   ├── capabilities/          # default.json (ACL whitelist)
│   ├── icons/                 # Tauri bundle icons
│   ├── tauri.conf.json
│   ├── Cargo.toml · Cargo.lock
├── tests/                 # Python compatibility tests (parity check)
├── bettercursor/, adapter/# Old Python daemon reference impl (archive)
├── vendored/              # Upstream Cursor parsing library (subrepo)
├── PRD.md · SYNC_DESIGN.md · AGENTS.md · docs/
└── .github/workflows/     # release.yml (3-OS matrix)

Architecture overview

Session reads happen in three layers (see SYNC_DESIGN.md §2.5 Q6 for UUID identity):

Layer Storage Path (Linux) Role
L1 JSONL ~/.cursor/projects/<slug>/agent-transcripts/<uuid>/<uuid>.jsonl Transcript; CLI + Desktop both write; same uuid as L2 when CLI session is valid
L2 SQLite ~/.cursor/chats/<md5(cwd)>/<uuid>/store.db CLI only (cursor-agent); Sidebar resume list on CLI
L3 SQLite KV ~/.config/Cursor/User/globalStorage/state.vscdb (cursorDiskKV: composerData:*, bubbleId:*; plus per-workspace workspaceStorage/*/state.vscdb) Desktop composer index + bubble bodies

Injecting Layer 3 (CLI → Desktop Sidebar)

Always quit Cursor Desktop first — writing state.vscdb while Cursor is running can corrupt the WAL. Then in bettercursor open the CLI session and run sync Layer 2/3; restart Cursor. v0.3.4+ rewrites stub bubbles when it detects CLI envelopes, [REDACTED] assistant text, or missing images that exist in Layer 2. Full playbook: SYNC_DESIGN §0.5.

Rust side (src-tauri/src/core/) handles:

  1. paths.rs — parse cursor user dir / chat_root MD5 etc.
  2. storage.rs — WAL-safe read: copy to tempfile::tempdir(), PRAGMA wal_checkpoint(TRUNCATE), then read-only open.
  3. canonical.rs — cross-layer merge, emit CanonicalSession.

Tauri commands exposed to the frontend:

Command Args Returns
list_sessions All sessions in the cache
sync_now usize count, fires sessions-updated event
get_conversation uuid Parsed Conversation with merged bubbles + source_path
get_resume_command uuid, source open -a Cursor --args --resume <uuid> or cursor-agent --resume <uuid>
sync_session_layer23 uuid, cwd SyncReport (wrote_layer2 / wrote_layer3 / skipped / duration_ms)
fix_orphans FixOrphansReport (scanned / fixed / skipped, auto-backups store.db)
delete_session uuid, cwd, slug DeleteReport (cursor_running / removed_l1 / removed_l2 / skipped_*)
watcher_status { active, dirs[], last_scan_at_ms }
platform_info <os>: <cursor_user_dir> (debug)
transport_list_peers PeerSummary[] from ~/.bettercursor/transports.json
transport_test peerId TestReport (ok / latency_ms / error?)
transport_push uuid, peerId PushReport (uuid / bytes_written / duration_ms)
transport_pull peerId, sinceMs? PullReport (peer_id / count / snapshots[])

Cross-device sync (historical v0.2.6 SSH first cut)

v0.2.6 shipped the Transport trait first cut around SSH/rsync. That design remains useful as implementation history, but it is no longer the recommended user path after the v0.3.7 LAN-only product decision. Configure one or more peers in ~/.bettercursor/transports.json:

{
  "peers": [
    {
      "id": "macbook",
      "kind": "ssh",
      "host": "[email protected]",
      "port": 22,
      "identity_file": "~/.ssh/id_ed25519",
      "remote_snap_dir": "~/.bettercursor/peers/bettercursor-main",
      "remote_hostname": "macbook-pro-m1"
    }
  ]
}

Then from devtools console:

await __TAURI__.invoke('transport_list_peers')          // → [{id:"macbook",...}]
await __TAURI__.invoke('transport_test', { peerId: 'macbook' })  // → {ok:true, latency_ms:42}
await __TAURI__.invoke('transport_push', { uuid: '<a session>', peerId: 'macbook' })
// ~/.bettercursor/peers/bettercursor-main/<host>/<uuid>.json now exists on the peer.
await __TAURI__.invoke('transport_pull', { peerId: 'macbook', sinceMs: 0 })
// → { peer_id: "macbook", count: 1, snapshots: [...] }

SSH safety flags baked in: BatchMode=yes (no interactive prompts) + StrictHostKeyChecking=accept-new (auto-trust new hosts, fail loud on key mismatch). The Transport trait is sync (not async_trait) in v0.2.6 — it migrates to async in v0.3.0 when the offline outbox lands. This section is kept for release-history context rather than current product guidance.

Pitfalls

1. React 19 + Zustand 5 infinite re-render

useShallow((s) => derived(s)) looks shallow-stable, but when the derived function does [...arr].sort(...) the inner array refs differ and the comparison falls through, triggering React 19's Maximum update depth exceeded bail-out.

Fix: move the derived value out of the selector, memoize in useMemo at the component level:

// ❌ Don't:
useStore(useShallow((s) => groupByProject(s.items)))

// ✅
const items = useStore((s) => s.items)
const grouped = useMemo(() => groupByProject(items), [items])

2. Tauri capabilities ACL is minimal by default

Without plugin-specific permissions, a frontend invoke('plugin:foo|bar') is rejected with a TypeError. Add the permissions explicitly in capabilities/default.json:

{
  "permissions": [
    "core:default",
    "opener:default",
    "fs:default",
    "clipboard-manager:allow-write-text",
    "clipboard-manager:allow-read-text"
  ]
}

3. WebKitGTK devtools off by default

You must opt in via a Cargo feature, otherwise the right-click menu only shows "Reload", no "Inspect":

tauri = { version = "2", features = ["devtools"] }

4. WebKitGTK Wayland black screen

Some compositors (Mutter / Hyprland) crash on the GPU composition path. Known workaround:

WEBKIT_DISABLE_DMABUF_RENDERER=1 \
WEBKIT_DISABLE_COMPOSITING_MODE=1 \
LIBGL_ALWAYS_SOFTWARE=1 \
pnpm tauri dev

5. pnpm 9.4+ rejects empty pnpm-workspace.yaml

If a pnpm-workspace.yaml exists at the repo root (even just for allowBuilds), it must declare a packages field. Without it, pnpm aborts with ERROR packages field missing or empty. Add at minimum:

packages:
  - "."

This bit us on the first v0.2.5 release run — all matrix jobs failed in 30 s.

Docs

File Content
PRD.md Product requirements v0.1 feature matrix + acceptance criteria
SYNC_DESIGN.md v0.2+ sync / cross-device design
docs/README.md Doc layout; local archive in docs/local/ (gitignored)

Roadmap

v0.2.5 (✅ done)  Cross-platform packaging · i18n · background sync ·
                 conversation records · repair orphan · delete
v0.2.6 (✅ done)  Cross-device sync — Transport trait first cut ·
                 SSH/rsync (T2) impl · 4 Tauri commands
v0.3.0 (✅ done)  ~/.bettercursor/unified.db · snapshot codec v4 ·
                 async Transport · Conflict 5-way
v0.3.1 (✅ done)  LAN mDNS pairing · outbox · sync loop
v0.3.2–v0.3.5 (✅ done)  Settings UI · L2/L3 enrichment · L3 soft delete
v0.3.6 (✅ done)  Cross-device sync hardening — see SYNC_DESIGN §10.4
v0.3.7a (✅ done)  Interaction performance cut-down
v0.3.7d (✅ latest tag)  Self-discovery filter · trusted-peer cleanup
v0.3.7c (✅ done)  Version-line alignment · release metadata cleanup
v0.3.7b (✅ done)  LAN discovery stabilization
v0.3.7 (⚪ next)     LAN manual-address pairing · virtual LAN support
v0.3.8+           T3/T4/T5 adapters · Doctor (deferred)

Acknowledgements

  • UI paradigm: farion1231/cc-switch
  • Old Python daemon (bettercursor/, adapter/) provided the parsing algorithm reference
  • vendored/cursaves/ (AGPL, read-only) and vendored/cursor-history/ (MIT, read-only) are upstream Cursor parsing library snapshots; borrowable algorithms are indexed in SYNC_DESIGN.md §11.5

Currently a personal/early-stage project. v0.2.6 is the first release that ships cross-device sync; the latest maintenance tag is v0.3.7d.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors