Add macOS menu bar app (apps/macos) with ledger-backed spend#478
Conversation
Relocate the Agent Limit menu bar app into the burn monorepo as apps/macos and wire it to the ledger: - Live limits: flame menu bar icon (orange→red, fills when over pace) + per-window burndown charts, read from the Claude/Codex usage APIs. - Spend: under each window, shows cost this period vs. last period in USD, read from the burn ledger. Cost isn't stored in the ledger, so it shells out to `burn summary --provider <p> --since <ISO> --json` rather than re-deriving pricing. Resolved via a login shell (nvm/Homebrew PATH) and throttled to every 5 min since each call runs an ingest pass (~5s). Hidden when burn isn't installed. - Provider map: Claude→anthropic, Codex→openai. Builds with `swift build` / `apps/macos/build.sh`. SwiftPM app, not part of the Cargo/pnpm workspaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis PR introduces a complete new macOS menu bar app ( ChangesBurn macOS App
Sequence Diagram(s)sequenceDiagram
participant Timer
participant UsageViewModel
participant ClaudeProvider as ClaudeProvider/CodexProvider
participant ProviderAPI as Provider REST API
participant UsageHistoryStore
participant BurnLedger
participant BurnCLI as burn CLI
Timer->>UsageViewModel: tick → refresh()
UsageViewModel->>ClaudeProvider: fetch()
ClaudeProvider->>ProviderAPI: GET usage endpoint (Bearer token)
ProviderAPI-->>ClaudeProvider: JSON metrics / 401 / 429
ClaudeProvider-->>UsageViewModel: ProviderStatus (.ok / .warning / .rateLimited)
UsageViewModel->>UsageHistoryStore: record(provider, metric, at: now)
UsageHistoryStore-->>UsageViewModel: [UsageSample] (deduplicated + persisted)
UsageViewModel->>UsageViewModel: BurndownBuilder.build(metric, samples) → BurndownData
UsageViewModel->>BurnLedger: cost(provider, since: windowStart) ×2
BurnLedger->>BurnCLI: burn summary --provider --since --output json
BurnCLI-->>BurnLedger: totalCost.total
BurnLedger-->>UsageViewModel: PeriodSpend (current + lastPeriod)
UsageViewModel->>UsageViewModel: update charts, spend, menuBarIcon
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fafbd2be1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Make ledger-backed spend work with no separate install, and wire up signed/notarized releases for the app. - Bundle a self-contained native `burn` (compiled from relayburn-cli) into AgentLimit.app/Contents/MacOS/burn. BurnLedger prefers this bundled helper (exec'd directly — no node/PATH needed) and only falls back to a `burn` on PATH for `swift run` dev builds. Spend now works out of the box and even builds the ledger from session logs on first run. - build.sh: `cargo build --release -p relayburn-cli` and copy the binary in (skipped with a warning if cargo is absent). release.sh signs the helper with a hardened runtime before the app. - .github/workflows/release-macos.yml: manual workflow_dispatch that builds, signs, notarizes, and publishes AgentLimit-arm64.dmg. Uses a macos-v* tag scheme + a moving macos-latest pointer for a stable download URL, so it never disturbs burn's own v* CLI releases or their "latest" pointer. Reuses the same Apple secrets as Pear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the macOS app from "Agent Limit" to "Burn" throughout: - Bundle: CFBundleName/DisplayName/Executable = Burn; identifier com.agentworkforce.burn. App is now Burn.app. - SwiftPM target/module AgentLimit → Burn (Sources/AgentLimit → Sources/Burn, AgentLimitApp → BurnApp). Application Support dir → Burn. - Bundled helper renamed burn → burn-cli to avoid colliding with the `Burn` executable on case-insensitive filesystems; BurnLedger looks up burn-cli. - DMG is Burn-arm64.dmg; release-macos.yml titles "Burn for Mac". - README + scripts updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverse the earlier naming: the CLI keeps the canonical `burn` name and the macOS utility becomes BurnOSX (display name still "Burn"). - App bundle BurnOSX.app, executable BurnOSX, id com.agentworkforce.burnosx, DMG BurnOSX-arm64.dmg. CFBundleDisplayName stays "Burn" so Finder/menu show "Burn". - Bundled CLI helper restored to `burn` (was burn-cli) — no collision now that the app executable is BurnOSX, not Burn. BurnLedger looks up `burn`. - build.sh decouples the SwiftPM target (Burn) from the bundled app name (BurnOSX), so the internal module/Sources stay Burn without churn. - release-macos.yml + README updated to BurnOSX-arm64.dmg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The menu bar label rendered its colored flame with ImageRenderer().nsImage *inside* `body`, producing a fresh NSImage on every render pass. That re-entrant SwiftUI render churns the MenuBarExtra status item — under the feedback loop it spawns menu bar items without end and locks up the machine. Render the flame in the view model instead, only when the (usage, offTarget) state actually changes, and cache it. `MenuBarLabel.body` now just displays the stable cached image — no rendering, no per-pass NSImage churn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Defense-in-depth for the class of bug that locked up the machine: if the menu bar label ever re-renders in a tight loop again, count the renders in a 1s sliding window and, past a threshold far above anything legitimate (240/s), log and NSApp.terminate. A regression now degrades to "the app quit" instead of "reboot the Mac". The watchdog touches no observed state, so it can't itself trigger a render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/release-macos.yml (1)
119-127:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPin upload-artifact action to full SHA.
Similar to the checkout action, pin
actions/upload-artifactto a commit SHA for supply chain security.Suggested fix
- name: Upload build artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: agentlimit-macos-${{ github.run_id }} path: apps/macos/dist/*.dmg if-no-files-found: warn retention-days: 30🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release-macos.yml around lines 119 - 127, The actions/upload-artifact action in the Upload build artifacts step is currently referenced by version tag (v4) instead of being pinned to a specific commit SHA. Replace the version tag reference in the uses field with the full commit SHA of the desired version to improve supply chain security. This ensures the exact version of the action that runs is immutable and verifiable, preventing potential compromises from future changes to the v4 tag.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release-macos.yml:
- Around line 21-24: Pin the actions/checkout action to its full commit SHA
instead of using the `@v4` tag reference to mitigate supply chain security risks.
Replace `@v4` with the full commit SHA corresponding to that version, and add
persist-credentials: false to the with block to prevent Git credentials from
being stored in the workspace.
In `@apps/macos/Sources/Burn/BurnLedger.swift`:
- Around line 85-99: The capture method waits indefinitely for the process to
exit via process.waitUntilExit(), which can cause the actor to hang if the burn
subprocess hangs. Implement a timeout mechanism for process execution by adding
a timeout interval (e.g., 5-10 seconds) and checking the process status within
that timeout window instead of waiting indefinitely. If the process has not
completed within the timeout period, terminate it and return nil to prevent
blocking subsequent requests.
In `@apps/macos/Sources/Burn/Providers.swift`:
- Around line 14-16: The synchronous I/O operations in Credentials.claude() and
Credentials.codex() are blocking the main thread when called from the MainActor
context in the refresh() method, causing UI stalls. Move the credential loading
calls (the guard statements checking Credentials.claude() and
Credentials.codex()) to execute on a background queue before entering the
MainActor-protected code path, ensuring synchronous I/O does not block the menu
bar UI during refresh or login operations.
In `@apps/macos/Sources/Burn/UsageHistory.swift`:
- Around line 34-37: The key function that builds cache keys by concatenating
provider.rawValue, metric.name, and reset time with "|" delimiters is brittle
because if metric.name contains the "|" character, it will break the key parsing
logic that expects exactly 3 parts when split by "|". Fix this by either
escaping or URL-encoding the metric.name before concatenating it into the key
string in the key(provider:metric:) function, or by using a structured key type
that doesn't rely on delimiter parsing.
In `@apps/macos/Sources/Burn/UsageViewModel.swift`:
- Around line 150-152: The spend throttling condition in the if statement
(checking `!spend.isEmpty` along with `lastSpendAt` and `spendInterval`)
bypasses throttling when burn lookups fail and leave `spend` empty, causing
retries every 60s instead of respecting the 5-minute throttle interval. Remove
the `!spend.isEmpty` check from this condition so throttling is applied
regardless of whether spend was successfully populated. Additionally, reset
`lastSpendAt` to nil in the `select(_:)` method to ensure immediate fetch when
switching providers.
---
Outside diff comments:
In @.github/workflows/release-macos.yml:
- Around line 119-127: The actions/upload-artifact action in the Upload build
artifacts step is currently referenced by version tag (v4) instead of being
pinned to a specific commit SHA. Replace the version tag reference in the uses
field with the full commit SHA of the desired version to improve supply chain
security. This ensures the exact version of the action that runs is immutable
and verifiable, preventing potential compromises from future changes to the v4
tag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 62ce81fd-7c19-450d-8cd5-55bf2d22f61f
⛔ Files ignored due to path filters (2)
apps/macos/Sources/Burn/Resources/claude.svgis excluded by!**/*.svgapps/macos/Sources/Burn/Resources/openai.svgis excluded by!**/*.svg
📒 Files selected for processing (22)
.github/workflows/release-macos.ymlapps/macos/.gitignoreapps/macos/App/AppIcon.icnsapps/macos/App/Info.plistapps/macos/LICENSEapps/macos/Package.swiftapps/macos/README.mdapps/macos/Sources/Burn/BrandIcon.swiftapps/macos/Sources/Burn/BurnApp.swiftapps/macos/Sources/Burn/BurnLedger.swiftapps/macos/Sources/Burn/Burndown.swiftapps/macos/Sources/Burn/BurndownChartView.swiftapps/macos/Sources/Burn/ContentView.swiftapps/macos/Sources/Burn/Credentials.swiftapps/macos/Sources/Burn/Models.swiftapps/macos/Sources/Burn/Providers.swiftapps/macos/Sources/Burn/UsageHistory.swiftapps/macos/Sources/Burn/UsageViewModel.swiftapps/macos/build.shapps/macos/release.shapps/macos/scripts/generate-icon.swiftapps/macos/scripts/make-icon.sh
Root cause of the "endless menu bar flames" + machine panic: SwiftUI's MenuBarExtra duplicates its status item when the app's scene body re-evaluates, spiraling into a status-item/render storm that locks up the machine. (The earlier in-body ImageRenderer made it worse, but MenuBarExtra is the core liability.) Replace MenuBarExtra with a single NSStatusItem created once in an AppDelegate (via NSApplicationDelegateAdaptor); the app exposes only an inert Settings scene. The flame image is rendered by the view model and mirrored onto the status button through the $menuBarIcon publisher — no SwiftUI render path touches the menu bar, so it cannot duplicate or storm. The popover hosts the existing SwiftUI ContentView via NSHostingController. Remove RenderWatchdog (no SwiftUI menu bar body left to guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CI pins - BurnLedger.capture: bound the wait with a 30s timeout (drain stdout on a background queue) so a hung `burn` can't wedge the actor. - Providers: load Claude/Codex credentials in a detached task so the synchronous `security`/file I/O doesn't stall the MainActor refresh. - UsageViewModel.loadSpend: throttle on time alone (set lastSpendAt up front) so failed/burn-missing lookups also back off to 5 min instead of retrying every 60s; reset lastSpendAt on provider switch. - UsageHistory.pruneStaleWindows: read the reset timestamp from the last "|" segment, robust to a "|" in a metric name. - release-macos.yml: pin actions/checkout + upload-artifact to commit SHAs and set persist-credentials: false. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve apps/macos conflicts from #478 landing on main: - BurnLedger: keep main's bundled-binary resolution + capture timeout, plus this branch's Summary/summary() and ingest-watch for the live tab. - ContentView: main's content + the Usage/Live tab wrapper. - Providers, UsageHistory, UsageViewModel, release-macos.yml: take main's (off-MainActor creds, throttle fix, key-parse fix, SHA-pinned actions). main's `burn summary` is now a read verb (ingest gated behind --ingest), which is exactly what the live poll assumes — so the short poll cadence is cheap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Brings the Agent Limit macOS menu bar app into the burn monorepo as
apps/macos, and wires it to the ledger so it shows spend alongside live rate-limit burndowns. This is the "Burn for Mac" front-end: limits (forward-looking, from provider APIs) + spend (retrospective, from the burn ledger).What it does
api.anthropic.com/api/oauth/usage) and Codex (chatgpt.com/backend-api/wham/usage) usage APIs using the CLIs' existing credentials.Why it shells out to
burnCost is not stored in the ledger — burn computes it from its pricing table at query time. Rather than re-derive that pricing in Swift (which would silently drift), the app calls
burn summary --provider <p> --since <window-start> --jsonand readstotalCost.total. "Last period" iscost(since lastStart) − cost(since thisStart)(burn has no--until).Details:
PATH(and thenodetheburnshim needs) work even when launched from Finder.burn summaryruns an ingest pass (~5s), and runs off the main actor so the UI isn't blocked.UsageMetric.idis a fresh UUID each refresh).anthropic, Codex→openai. Hidden entirely ifburnisn't installed.Layout / build
apps/macos/, a SwiftPM app — not wired into the Cargo/pnpm workspaces (own toolchain).swift buildorapps/macos/build.sh→AgentLimit.app.apps/macos/release.shsigns/notarizes/packages a DMG.Follow-ups (not in this PR)
release.shworks; arelease-macos.ymlbuildingapps/macosis the natural next step.burn/relayburn-sdkbridge in the.appso spend works with zero install/PATH assumptions.AgentWorkforce/limitPR Add Codex session reader and CLI integration #1 (same app, now homed here).🤖 Generated with Claude Code