Pulse
qunqin24/Pulse/CLAUDE.md
AI entry for this repo. Human workflow, evidence rules, and the docs map live in CONTRIBUTING.md and Docs/README.md. Change the authoritative topic doc in the same patch as the code; do not grow this file. Pulse is a macOS menu-bar usage monitor. SwiftUI draws the panel; a transparent, non-activating AppKit NSPanel owns size, placement, and input. Seventy-seven providers, no Pulse backend, no Pulse account. Per-provider routes, auth, cookies, and extra logins: Docs/providers/README.md. Sources sit in six directories under Sources/Pulse: App…
CLAUDE.md482 starsChanged 4 days ago
# CLAUDE.md
AI entry for this repo. Human workflow, evidence rules, and the docs map live in [CONTRIBUTING.md](CONTRIBUTING.md) and [Docs/README.md](Docs/README.md). Change the **authoritative topic doc** in the same patch as the code; do not grow this file.
Pulse is a macOS menu-bar usage monitor. SwiftUI draws the panel; a transparent, non-activating AppKit `NSPanel` owns size, placement, and input. Seventy-seven providers, no Pulse backend, no Pulse account. Per-provider routes, auth, cookies, and extra logins: [Docs/providers/README.md](Docs/providers/README.md).
Sources sit in six directories under `Sources/Pulse`: **App** (lifecycle, settings store, updates), **Panel** (the window, its placement, and everything drawn in it), **Settings** (the settings window), **Usage** (store, cache, ledger, forecast, alerts), **Providers** (one service per product, plus their helpers), **Auth** (keys, logins, cookies). Swift has no directory namespace and SwiftPM recurses, so a file's directory is a claim about what it belongs to and nothing else — move a file when that claim stops being true.
## Commands
```bash
swift run Pulse
swift build
./Scripts/bundle.sh
./Scripts/dmg.sh
./Scripts/check-localization.sh
swift test
./build.noindex/Pulse.app/Contents/MacOS/Pulse --json
```
**`xcode-select` must point at Xcode, not CommandLineTools.** `#Preview` is expanded by an Xcode plugin; otherwise every build fails with `PreviewsMacros plugin not found`. Check with `xcode-select -p`. One-off: `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build`.
**Never strip `#Preview` blocks to make the build green.** That hides errors inside them and has already shipped a broken build. See [Docs/decisions/never-strip-previews.md](Docs/decisions/never-strip-previews.md).
A clean `swift build` is not the Xcode check. Actor-isolation mistakes can be warnings here and hard errors there (every `View` is `@MainActor`). Before claiming a change builds:
```bash
swift build
```
Treat remaining warnings as failures. macOS 14+, Swift tools 6.0, no linter. **`swift test` exists** — rules, cache reconciliation and provider fixtures; run it, and add to it when you change a rule. [Docs/testing.md](Docs/testing.md). CI and release need the macOS 26 SDK (`glassEffect`). **The SDK version stamped into the binary decides which control design macOS draws** — `Package.swift` sets it in `linkerSettings` because SwiftPM stamps the deployment target instead, and `bundle.sh` reads it back off every slice. Below 26 the app is silently drawn the old way. [Docs/decisions/sdk-stamp-and-appearance.md](Docs/decisions/sdk-stamp-and-appearance.md)
## Do not violate
- **Panel frame while a card opens:** do not resize the window and do not put card + rail in a shared stack. Axis change (side ↔ top) *may* resize. [Docs/ui/panel-geometry.md](Docs/ui/panel-geometry.md)
- **Hover:** SwiftUI `.onHover` does not work on this accessory, non-key panel. Enter from tracking areas, leave from sampling the pointer — never exit events. Drag and ring clicks belong to `FloatingPanel.sendEvent`. `hitTest` / synthesised events are not proof of real input. [Docs/ui/input.md](Docs/ui/input.md)
- **Percentages:** Pulse does not invent usage percentages. If a provider reports none, say so. Labelled exceptions only, each marked as inferred on screen and each drawing **nothing** rather than a zero when its inputs are missing: the money estimate; Command Code's monthly plan grant (reported remainder, unreported plan size); and every API account or extension that reports money and no allowance (`BalanceRing`, DeepSeek's rule first) — its denominator is one Pulse watched, one the reader typed, or none, and only the provider's own flag may call it spent. [Docs/refresh-and-data.md](Docs/refresh-and-data.md), [Docs/providers/command-code.md](Docs/providers/command-code.md), [Docs/providers/deepseek.md](Docs/providers/deepseek.md)
- **Localization:** only `String.localized(_:)` / `Text(localized:)`. No implicit `Text("…")`. No conditionals inside `localized:`. Interpolate `String`, not `Int` (`%lld` vs `%@`). Run `./Scripts/check-localization.sh`. **Translations are written, not converted**: no English syntax, no term the reader has no word for, and a control agrees with its own sub-settings. The script checks keys, not whether the copy reads. [Docs/development.md](Docs/development.md)
- **Notifications say nothing Pulse did not witness.** `spent` only when the provider says so, a reset only on unambiguous evidence, an unavailable reading is not automatically a failure, and a **push** route going quiet (Claude Code's status line) is not one either. A limit already past the line when the setting goes on **is** announced, once — silence then a wall is the feature failing. All off by default, all silent. `UNUserNotificationCenter` **raises without an app bundle** — every entry point is fenced by `UsageAlerts.isSupported`, so test from `./Scripts/bundle.sh`, never `swift run`. [Docs/notifications.md](Docs/notifications.md)
- **Disabled providers are not fetched.** Shared `Unavailability` copy names no provider.
- **Layout constants are budgets** (`PanelMetrics` computed `var`, never `static let`). Anything new on the panel takes size from them.
- **Do not fetch or document provider auth here.** [Docs/providers/README.md](Docs/providers/README.md)
- **Pulse reads; the window starter is the one exception.** It sends a single "hi" through the provider's own CLI, off by default, switched on only through the risk confirmation, nothing saved. Nothing else may send on the user's behalf. [Docs/window-starter.md](Docs/window-starter.md)
## Read by task
| Task | Read |
|---|---|
| Shell, settings, login, defaults | [Docs/architecture.md](Docs/architecture.md) |
| Panel size, dock, overlay, scale | [Docs/ui/panel-geometry.md](Docs/ui/panel-geometry.md) |
| Drag, hover, clicks, hit testing | [Docs/ui/input.md](Docs/ui/input.md) |
| Glass, rings, colour, activity mark | [Docs/ui/rings-and-surface.md](Docs/ui/rings-and-surface.md) |
| Settings window copy/layout | [Docs/ui/settings.md](Docs/ui/settings.md) |
| Refresh, cache, activity, ledger | [Docs/refresh-and-data.md](Docs/refresh-and-data.md) |
| Token spend pane, agents, spend readers | [Docs/token-spend.md](Docs/token-spend.md) |
| Notifications, alert rules | [Docs/notifications.md](Docs/notifications.md) |
| What is tested, fixtures | [Docs/testing.md](Docs/testing.md) |
| `--json` output contract | [Docs/json-output.md](Docs/json-output.md) |
| Developer integrations, account links | [Docs/integrations.md](Docs/integrations.md) |
| Starting usage windows after a reset | [Docs/window-starter.md](Docs/window-starter.md) |
| Localization, resources, adding UI | [Docs/development.md](Docs/development.md) |
| Bundle, tag, Sparkle, DMG | [Docs/releasing.md](Docs/releasing.md) / [Docs/build-from-source.md](Docs/build-from-source.md) |
| Why / failure lessons | [Docs/decisions/README.md](Docs/decisions/README.md) |
| Provider routes / extra accounts | [Docs/providers/README.md](Docs/providers/README.md) |
## Current facts (verify in code if they matter)
Seventy-seven built-in providers (`Provider.builtIn`) — twenty-five written case by case, fifty-two as `ProviderProfile`s under `Providers/Profiled/` ([Docs/providers/README.md](Docs/providers/README.md#adding-a-provider)) — plus `.pulseExtension`, which carries every user-added extension as an account ([Docs/extensions.md](Docs/extensions.md)). Extra accounts: Claude Code, Codex, Grok, Grok Bot (`supportsMultipleAccounts`). Adaptive refresh is a **one-shot** timer, **2–30 minutes** (`AdaptiveRefresh.floor` 120s / `ceiling` 1800s) — not a 60s loop. Liquid Glass drag: hit-testable `PanelSurface`, window `sendEvent`, and `PanelGlass.claim` under the glass so the window owns its pixels — verified by real input (#68).
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

