OpenUsage is a SwiftPM-based SwiftUI menu-bar app for macOS that shows AI provider usage widgets (Claude, Codex, Cursor, Grok, Devin, and more).
This file documents the engineering conventions for the project. Read it before contributing.
AGENTS.md is the source of truth for agent instructions in this repository. CLAUDE.md files may only point to the nearest AGENTS.md file with @AGENTS.md; do not add guidance, duplicate instructions, or project rules to CLAUDE.md.
Repository note: This is the native Swift edition of OpenUsage. Active development happens on the
mainbranch. (NOT the legacy Tauri version which now sits in thetauri-legacybranch)
main is the active development line; it ships via .github/workflows/release.yml (Sparkle appcast on gh-pages). Cut releases with the release-swift skill.
- Versions are
0.7.xand up. Never reuse a0.6.xnumber — those are the original edition's released tags, now frozen on thetauri-legacybranch (final releasev0.6.28). - Never increase the version number on your own initiative — always ask for explicit approval first. The version is a deliberate owner decision: propose the number and wait for explicit sign-off before tagging or cutting a release.
- Beta releases use
-beta.Ntags and stay GitHub pre-releases on Sparkle's beta channel. Stable releases use plain tags and become GitHub "Latest". - Stable releases must carry forward the legacy
latest.jsonso any remaining0.6.xinstalls can still update tov0.6.28.release.ymlhandles this; verify it with the release-swift skill. - Never leave a release in Draft, and never ship blank notes: the release-swift skill generates the changelog and verifies the published release after every cut.
- SwiftPM executable target; SwiftUI content hosted in an AppKit-owned
NSStatusItem+ custom key-capableNSPanel. - Swift 6 with strict concurrency.
- Providers implement the small
ProviderRuntimeprotocol: an auth store reads credentials already on the user's machine, a usage client calls the provider's API, and a mapper normalizes the response intoMetricLinevalues. The UI renders those normalized values. - See
docs/for behavior docs and the developer docs (architecture overview, adding a provider).
Conventions for the per-provider modules under Sources/OpenUsage/Providers/<Name>/.
- Structure: one folder per provider with an auth store (reads credentials already on the user's machine), a usage client (calls the provider API), and a mapper (normalizes to
MetricLine), conforming toProviderRuntime—refresh()plushasLocalCredentials(), the local-only credential probe used by first-run detection (FirstRunSeeder) and by new-provider detection on the first launch after the provider ships (NewProviderSeeder); mirror the same local credential sources and usability filters thatrefresh()starts with, reusing the auth-store loaders instead of adding a second credential-reading path. Seedocs/adding-a-provider.mdanddocs/provider-enablement.md. - Model pricing: all spend imputation (Claude, Codex, Cursor, Grok) prices through the shared engine in
Sources/OpenUsage/Pricing/(seedocs/pricing.md). Cursor-native model rates and alias rules live inSources/OpenUsage/Resources/pricing_supplement.json— sync new or changed models from Cursor models & pricing (updateupdated_at, pricing entries, andalias_rulesfor CSV model slugs); merging tomainpublishes it to gh-pages, so installed apps pick it up without a release. The bundled LiteLLM/models.dev snapshots regenerate withscript/update_pricing_snapshots.sh(a release-time chore). - Default order: Claude, Codex, Cursor first (the established providers, in that order), then every other provider alphabetically by display name (Antigravity, Devin, Grok, …). The order is the array order in
AppContainer, which seedsLayoutStore's default provider order (andresetToDefault). A new provider slots into the alphabetical tail. - Metric placement defaults: when adding or changing a metric, confirm its four defaults with the owner before choosing — never pick silently:
- enabled on/off (
DefaultLayout.metricIDs), - Always Visible vs. On Demand — above the fold vs. behind the per-provider caret (
DefaultLayout.expandedMetricIDs). Note: a provider always keeps at least one Always Visible row — the dashboard promotes all metrics when every one is marked On Demand, so a fully On Demand provider isn't possible; leave one metric Always Visible for the caret to appear, - pinned to the menu bar (
DefaultLayout.pinnedMetricIDs), - order (within a provider, the
widgetDescriptorsdeclaration order).
- enabled on/off (
- There is no hot reload. The app is a long-lived menu-bar process, so every code change requires a full rebuild and restart of the running app to take effect — kill the running instance, rebuild, and relaunch before testing.
Every PR description must follow this structure so reviewers can skim it quickly:
- TL;DR — open with a one- or two-sentence plain-English summary of the change.
- What was happening — plain-English bullet points describing the prior behavior, bug, or gap that motivated the change.
- What this changes — bullet points describing what the PR actually changes.
- Heads-up (optional) — noteworthy things a reviewer or future maintainer should consider (risks, follow-ups, trade-offs).
- Tests (optional) — how the change was verified.
- Screenshots (optional in general, but required for any PR that makes a visual change) — images of the affected UI after the change.
- Logic changes must update any docs in
docs/that describe the affected behavior. - Keep docs simple, less-technical, and easy to skim; exclude visual design details.
- Add a regression test when fixing a bug, where it fits.
- Keep files under ~500 LOC; split or refactor as needed.
- No new dependencies without justification.
- When adding a provider, follow the conventions in "## Providers".
Always fail loudly into error logging (log file, PostHog) and show friendly errors to the user. Do not add silent fallbacks that hide real problems. Only validate at system boundaries (user input, external APIs); trust internal code and framework guarantees.
- Use title case for any hardcoded copy used as a title.
- Match the existing design language; OpenUsage has a specific look and feel.
- Only add tooltips (
hoverTooltip) when explicitly asked to. Don't add them proactively to new controls.