Prototype: Services as composable state machines - #190
Draft
kyleve wants to merge 8 commits into
Draft
Conversation
## What A new native-macOS menu bar app, **Ledger**, that shows your current-cycle Cursor spend at a glance. Building it also **restores the native-macOS build infrastructure** removed with Foreman (#68) — the `.macOS` platform, a `.mac` app target + hostless macOS test bundle, the platform-scoped CI scheme, and the `test-macos` job. The menu-bar item shows the current-cycle amount (cents dropped, digits roll over on change). Clicking opens a popover with: - **This cycle** spend + billing-cycle date range, and the plan tier as a badge. - **Today** and **this week** spend (differenced from locally recorded history — hidden until enough history exists). - **Included usage** as two bars — first-party/Auto vs third-party/API — since a single blended figure hides that one pool can be maxed while the other is barely used. - **Top models this cycle** as usage shares (each model ≥5% gets its own bar; smaller ones roll into one multi-colored "Other models" bar with a legend), with model ids parsed into friendly names + badges (e.g. `Claude Opus 5` · `high`). - A **Refresh** button, **Settings**, and **Quit**. ## Data source Works for **individual** accounts by calling the same undocumented endpoints `cursor.com/dashboard` uses (the team Admin API only sees team accounts): - `GET /api/usage-summary` — cycle dates, plan, included-usage pool percentages, and live usage-based spend (`individualUsage.onDemand.used`). - `POST /api/dashboard/get-filtered-usage-events` — individual events (per-event model + `chargedCents`), paginated over the cycle and summed per model for the breakdown. Must be called **without** `teamId` for an individual account, or it 401s. **Auth is zero-config**: it auto-detects your Cursor session read-only from the local app's `state.vscdb` (`cursorAuth/accessToken`) and builds the required `WorkosCursorSessionToken=<userId>::<jwt>` cookie, deriving `userId` from the JWT `sub` (a bare JWT 401s). Settings › Account has an optional paste-a-token override (Keychain-stored) for when auto-detect fails or the session expires. The resolved token is cached and re-read only when it can actually change — the pasted value is edited, the API returns 401, or Settings appears. ### Today / this week `onDemand.used` is a cycle-cumulative running total, so `SpendHistory` records timestamped samples (`SpendHistoryStore`, JSON in Application Support, pruned to ~14 days) and **differences** them — today = since local midnight, this week = since the start of the calendar week. These are real billed dollars, and the diff captures spend even while the app was closed, as long as a sample exists near the window start. Each figure is hidden until there's a baseline. ## Cost & resilience - The headline refreshes every 5 minutes (configurable in Settings); opening the popover does not fetch. The **per-model breakdown is throttled separately** to at most every 15 minutes, since it walks every usage event in the cycle — an explicit Refresh or a cycle rollover bypasses the throttle. - A **failed refresh keeps the last data on screen** and turns the "Updated…" caption into an amber stale warning (reason on hover) rather than blanking. The full error screen shows only before anything has loaded. A per-model failure likewise keeps the last good breakdown. - Only the newest request may mutate state (including recorded history), so a superseded response can't skew day/week baselines. - The API gets a **dedicated ephemeral session with no cookie storage** and a 30s timeout: auth is the `Cookie` header we set explicitly, and URLSession must never substitute a stored one. ## Structure - **`Ledger/LedgerCore`** (macOS-only SPM library) — `LedgerServices` (`@MainActor @Observable` root, single `LoadState`), `SessionToken`/`SessionTokenSource`/`CursorLocalTokenSource`, `KeychainStore`, `DashboardProvider`/`CursorDashboardAPI`, wire/view models (`UsageSummary`, `UsageEvent`/`UsageEventsPage`, `SpendSnapshot`, `ModelShare`, `ModelName`), `SpendHistory`/`SpendHistoryStore`, and `LoginItemController` (launch-at-login, restored from Foreman). - **`Ledger/Ledger`** (`.mac`, `LSUIElement`) — AppKit `NSStatusItem` + `NSPopover` shell hosting a SwiftUI `MenuBarLabel` (bound to the observable session; deliberately not `MenuBarExtra`), a System-Settings-style sidebar, and the thin `LedgerSession` facade. - **`Ledger/install`** — builds a Release, ad-hoc-signs it, and installs to `/Applications` so it runs standalone (no Xcode). - Build wiring: `.macOS(.v26)` + `LedgerCore` in `Package.swift`; `Ledger`/`LedgerCoreTests` targets and `Ledger`/`Ledger-macOS-Tests` schemes in `Project.swift`; the `test-macos` CI job; root `AGENTS.md` + per-module `README.md`/`AGENTS.md`. ## Notes / caveats - The dashboard endpoints are **undocumented** and can change without notice (e.g. `get-aggregated-usage-events` went stale for some accounts — returning days-old data and omitting newly released models — which is why the breakdown uses the per-event endpoint). - Cursor's `state.vscdb` is read **strictly read-only** (`SQLITE_OPEN_READONLY`, one parameterized `SELECT` against `ItemTable`); we never write to it. If the key or path ever moves, auto-detect degrades to "no session found" and the paste fallback takes over. - It rides your Cursor **web session** (re-open Cursor when it expires). - The per-model figures are **usage shares, not spend**: their summed per-event cost is total usage value (included allowance + on-demand), which exceeds the billed on-demand headline. - There is **no year-to-date total** — the monthly-invoice endpoint is a billing ledger with cross-month credit/adjustment lines, so summing it isn't a meaningful yearly figure. ## Verification - `./swiftformat --lint` — clean - `mise exec -- tuist generate --no-open` — clean - `mise exec -- tuist test Ledger-macOS-Tests -- -destination 'platform=macOS'` — full LedgerCore suite passes: parsing/decoding, JWT→cookie derivation, a real SQLite round-trip for the token reader, history differencing, stale-vs-error state, per-model aggregation and pagination, the throttle, token caching/invalidation, and a `URLProtocol`-stubbed check of the outgoing request shape (no `teamId`, epoch-ms dates, the session cookie) — mutation-checked to confirm it fails when the rule is broken. - `mise exec -- tuist build Stuff-iOS-Tests` — iOS scheme unaffected - The auth-path changes (cookie-less session, token caching) were additionally verified against the live API after installing, since unit tests can't exercise them.
The Ledger PR (#103) landed LedgerLog on PeriscopeCore, but AGENTS.md and README.md still described LogKit and an OSLog subsystem. Align the module docs with WhereCore's Periscope wording. Validation: documentation-only; no tests run. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Add an exploratory ServicesStateMachine layer under WhereCore/Prototype that models five TLA-aligned protocol slices (tracking, post-write, ingestor quiesce, launch, reset) as pure reducers over a composite ServicesSnapshot. Includes scenario replay tests and a README — not wired into production WhereServices. Co-authored-by: Cursor <cursoragent@cursor.com>
> _Posted by an AI agent on kve's behalf._ ## Summary - persist the primary Location-card counts last presented for each year - hold saved counts until the card surface is visible and unobscured, then animate to the current report - emit one light haptic when one or more visible counts increased; keep decreases, first visits, and newly appearing cards silent - clear presentation history on reset and cover the behavior with focused persistence and presentation-model tests ## Design notes The reconciliation task is attached to the card surface and keyed by its counts, year, and explicit visibility. Switching tabs, pushing a destination, or presenting the Resolve sheet leaves the saved baseline untouched; returning to unobscured cards reconciles against the latest report. Multiple increased cards intentionally produce one coordinated haptic event. ## Testing - `./swiftformat --lint` - `./test --all` (1,629 tests passed before the focused review commits) - `./test WhereUITests` (350 tests passed after review fixes) - `./test --snapshots` (33 tests passed after review fixes; references unchanged) - `git diff --check`
> _Posted by an AI agent on kve’s behalf._ ## Summary - add a compact passport-style GitHub link with a subtle accent glow at the bottom of Settings > About - share the guilloché rosette renderer with the existing Locations cards while keeping footer styling independently tokenized - add in-file previews required by the architecture lint - capture every About snapshot at intrinsic full-content height across the existing iPhone and iPad matrices - teach SnapshotKitTesting to measure UIKit-backed SwiftUI forms from their root scroll content, with a regression test - localize the new copy, extend Settings search terms, and refresh About documentation ## Testing - ./swiftformat --lint - ./xcstrings --lint - swift run bumper lint . --timings - ./test WhereUITests (345 tests passed) - ./test SnapshotKitTestingTests (41 tests passed) - focused About snapshot matrix (16 configurations passed after recording and visual review) - focused Locations snapshot matrix passed
## Summary - add an inset “Private by design” passport card at the top of Data and About - state the privacy model directly: location history stays on the user’s devices and in their private iCloud account, and Where sends no data to anyone - render the privacy card as a passport-navy, gold-accented reflective surface using the location cards’ snapshot- and Reduce Motion-aware tilt treatment - keep the security-print rosette and Liquid Glass treatment independent from motion availability, so the complete surface remains visible before or without a tilt sample - share the passport-card structure with the GitHub link while retaining standard Form insets and the native two-shadow treatment from main; the privacy variant uses a quieter dark-navy glow - cover the complete Data and About pages with intrinsic-size iPhone and iPad snapshots across appearance, contrast, and accessibility variants ## Testing - ./test WhereUITests (354 tests passed before the visual follow-ups) - focused PassportCardSurfaceKindTests, PassportCardSurfaceTests, PassportCardTests, PrivacyPassportCardTests, and passportCardStyle tests - focused AboutSettingsViewSnapshotTests and DataSettingsViewSnapshotTests - ./swiftformat --lint - ./xcstrings --lint - swift run bumper lint . --timings
…ices-state-machines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Look-only prototype — not wired into production.
What this is
An exploratory sketch of redefining WhereServices coordination as composable state machines:
Sub-machines (TLA-aligned)
TrackingMachineTrackingReconciliationPostWriteMachinePostWriteReconcileIngestorMachineIngestorQuiesceLaunchMachineLaunchLifecycleResetMachineWhereServices.reset()orderingPlain actors (
ReportReader, backup export, recent activity, …) stay out of scope.How to read it
Where/WhereCore/Sources/Prototype/ServicesStateMachine/ServicesSnapshot.swiftServicesMachine.reduce— one event in, snapshot + effects outMachines/*.swift— maps 1:1 to an existing TLA READMEServicesStateMachineTests— replays Coalesced.cfg / Current.cfg scenariosDocs:
Where/WhereCore/Prototype/ServicesStateMachine/README.mdStacked on
If this were real
A production runner would replace ad-hoc
Taskloops inWhereSession/DayJournalincrementally — one protocol at a time — not a big-bang rewrite.