Skip to content

Add Claude support - #1

Merged
divyanshub024 merged 6 commits into
mainfrom
dv/claude-support
May 7, 2026
Merged

Add Claude support#1
divyanshub024 merged 6 commits into
mainfrom
dv/claude-support

Conversation

@divyanshub024

@divyanshub024 divyanshub024 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add Claude as a second usage provider alongside Cursor.
  • Refactor dashboard, settings, and menu bar state to handle multiple providers cleanly.
  • Stabilize Claude usage cards so All models and Claude Design render once in a fixed order.
  • Document the upcoming v0.4.0 release and the missing v0.3.0 changelog entry.

Details

  • Uses an isolated local Claude web session and validates Claude URLs before loading web content.
  • Parses Claude usage percentages and reset timing from the signed-in usage page.
  • Hides disconnected provider cards and keeps the menu bar progress based on the highest connected provider usage.
  • Keeps the loading UI visible while saved provider sessions are still being checked.
  • Adds launch-at-login support plus local development and notarized DMG verification helper scripts.

Validation

  • xcodebuild -project AIMeter.xcodeproj -scheme AIMeter -destination platform=macOS -derivedDataPath ./.derived test
  • git diff --check
  • bash -n scripts/run_dev.sh scripts/build_verify_notarized_dmg.sh scripts/build_dmg.sh

Summary by CodeRabbit

  • New Features

    • Added Claude support with local session management and usage parsing
    • Introduced launch-at-login option via macOS login items
    • Implemented provider-aware dashboard state displaying both Cursor and Claude metrics
  • Changed

    • Menu bar progress now reflects the highest connected provider percentage
    • Popover filtering updated to show only connected providers while maintaining loading indicators
  • Bug Fixes

    • Deduplication of Claude usage rows
    • Prevention of invalid usage text display
    • Fixed first-run connection screen flash issue

Deduplicate Claude usage metrics so the popover always shows one All models card and one Claude Design card in a fixed order.

Keep the loading UI visible while provider state is still being loaded, instead of briefly showing the Connect a Provider screen on relaunch.

Add parser regression tests for repeated and reordered Claude usage rows.
Document the upcoming v0.4.0 Claude support release, including provider-aware dashboard updates, stable Claude usage cards, startup loading behavior, login item support, and helper scripts.

Add the missing v0.3.0 changelog entry for the compact menu bar progress-only status item.
@divyanshub024
divyanshub024 marked this pull request as ready for review May 7, 2026 13:46
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@divyanshub024 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 8 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8fd1312c-d7b7-4a2e-90b2-f6245c11da27

📥 Commits

Reviewing files that changed from the base of the PR and between adefe2f and 8e85902.

📒 Files selected for processing (1)
  • Sources/AIMeter/Claude/ClaudeSessionManager.swift
📝 Walkthrough

Walkthrough

This PR implements Claude as a multi-provider parallel to Cursor, refactoring the app architecture to support generalized provider protocols and data models. It adds comprehensive Claude web scraping via WKWebView with DOM polling and network interception, introduces macOS launch-at-login functionality, and unifies Cursor and Claude into a shared dashboard UI with provider-aware status, metrics, and controls throughout the popover and settings views.

Changes

Claude Multi-Provider Support & Launch-at-Login Implementation

Layer / File(s) Summary
Data Models & Settings Structures
Sources/AIMeter/Core/Models.swift, Sources/AIMeter/Core/LaunchAtLoginController.swift
Introduces UsageProvider enum (cursor/claude), ProviderUsageSnapshot as generalized snapshot model, UsageMetric with optional percent, ProviderConnectionState, and refactors DashboardState to store providerSnapshots per provider with menu-bar progress aggregation; adds ClaudeSettings and LaunchAtLoginStatus enum.
Generalized Provider Protocols
Sources/AIMeter/Core/CursorUsageClient.swift, Sources/AIMeter/Core/CursorUsageCoordinator.swift
Replaces cursor-specific CursorUsageClient/CursorUsageError/CursorConnectionState with ProviderUsageClient/ProviderUsageError/ProviderConnectionState; refactors ProviderUsageCoordinator to support any provider with shared connect(), disconnect(), refresh(), fetchAndStoreSnapshot(), and applyFailureState() logic; retains typealiases for backward compatibility.
Multi-Provider Dashboard Store
Sources/AIMeter/Core/DashboardStore.swift
Maintains snapshotsByProvider dictionary, subscribes to both cursor and Claude coordinators, aggregates snapshots for all providers, computes highest progress across non-expired providers for menu bar.
Claude URL Validation
Sources/AIMeter/Claude/ClaudeURLValidator.swift
Validates Claude usage URLs against allowlist (claude.ai, www.claude.ai), enforces HTTPS, rejects user/password/port, sanitizes to settings path or defaults.
Claude Dashboard Parser
Sources/AIMeter/Claude/ClaudeDashboardParser.swift
Parses Claude dashboard DOM text and JSON response bodies, extracts usage metrics and reset values via regex and contextual inference, deduplicates metrics, canonicalizes titles for usage-settings URLs, returns ParseResult.usage/authRequired/noMatch.
Claude Session Manager
Sources/AIMeter/Claude/ClaudeSessionManager.swift
Manages Claude web sessions via ClaudeSessionManaging protocol; supports interactive (connect) and background (fetchUsage) flows; handles website data cleanup on disconnect to remove claude-related persistent data.
Claude Web View Scraper
Sources/AIMeter/Claude/ClaudeWebViewScraper.swift
Loads Claude in WKWebView with dual extraction: periodic DOM polling and network interception via JS monkey-patching; injects login-assistant script to suppress/disable Google auth; implements timeout recovery with final DOM poll; manages popup windows for auth flows; resolves single ProviderUsageSnapshot.
Claude Dashboard Client
Sources/AIMeter/Claude/ClaudeDashboardClient.swift
Implements ProviderUsageClient for Claude; validates usage-page URL, caches connected snapshot, fetches fresh usage via session manager when cache empty or missing progress percent.
Launch at Login Controller
Sources/AIMeter/Core/LaunchAtLoginController.swift
Manages macOS login items via SMAppService.mainApp; exposes isEnabled and statusMessage; registers on first run (configurable via shouldEnableByDefault); refreshes state and handles errors.
Settings Persistence
Sources/AIMeter/Storage/SettingsStore.swift
Persists Claude settings, migrates legacy Cursor configuration, sanitizes Claude URLs via validator, merges with defaults per provider.
App Environment Wiring
Sources/AIMeter/App/AppEnvironment.swift
Constructs Claude session manager, client, and coordinator alongside Cursor; passes both coordinators to DashboardStore, SettingsWindowController, and MenuBarController.
App Lifecycle
Sources/AIMeter/App/AppDelegate.swift, Sources/AIMeter/App/AIMeterApp.swift
Starts and stops Claude coordinator on app launch/termination; passes Claude coordinator and launch-at-login controller into settings view.
Menu Bar Status & Popover Control
Sources/AIMeter/UI/MenuBarController.swift
Stores Claude coordinator; dynamically sizes popover by connected provider count; computes primary connection state with precedence (authExpired > syncFailed > connected); generates tooltip from all connected providers or prompts user to connect.
Multi-Provider Popover Dashboard
Sources/AIMeter/UI/MenuPopoverView.swift
Displays initial-loading state when provider state is being verified, first-run onboarding with provider-parameterized tiles, or unified dashboard with ForEach over connected provider snapshots; renders provider-generic sections with shared connection/progress/metric logic and provider-specific action handlers.
Multi-Provider Settings UI
Sources/AIMeter/UI/SettingsView.swift, Sources/AIMeter/UI/SettingsWindowController.swift
Adds launch-at-login toggle and status message; replaces cursor-only settings with reusable provider settings sections for both Cursor and Claude; passes Claude coordinator and launch-at-login controller into view.
Claude Client Tests
Tests/AIMeterTests/ClaudeDashboardClientTests.swift
Tests URL validation, configuration error rejection, and cached snapshot behavior.
Claude Parser Tests
Tests/AIMeterTests/ClaudeDashboardParserTests.swift
Tests usage extraction from DOM/JSON, deduplication, reset parsing, auth detection, and edge cases.
Claude URL Validator Tests
Tests/AIMeterTests/ClaudeURLValidatorTests.swift
Tests allowed-host acceptance, unsafe URL rejection, and sanitization fallback.
Coordinator Tests
Tests/AIMeterTests/CursorUsageCoordinatorTests.swift
Tests connect snapshot refresh, multi-provider failure state merging, and provider-aware mock client.
Dashboard Store Tests
Tests/AIMeterTests/DashboardStoreTests.swift
Tests multi-provider snapshot tracking, highest-progress aggregation, connected-provider filtering, and per-provider disconnect independence.
Launch at Login Tests
Tests/AIMeterTests/LaunchAtLoginControllerTests.swift
Tests login item registration once-only behavior, enable/disable transitions, error handling, and status UI mapping.
Settings Store Tests
Tests/AIMeterTests/SettingsStoreTests.swift
Tests legacy Cursor migration and Claude default initialization.
Xcode Build Project
AIMeter.xcodeproj/project.pbxproj
Adds Claude source files, tests, and LaunchAtLogin controller to build targets.
Release Documentation & Ignore
CHANGELOG.md, .gitignore
Documents 0.4.0 release; ignores scripts directory.
Deprecated Scripts
scripts/build_dmg.sh, scripts/generate_dmg_background.swift, scripts/release_github.sh, scripts/generate_app_icon.swift
Removes automated DMG and release scripts; scripts directory now ignored.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through Claude's new domain,
Two providers dance in the dashboard pane,
From login screens to metrics bright,
Multi-provider magic, shimmering light! 🐰✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description covers the summary and detailed changes but lacks the verification checklist completion markers and notes section completion that the template requires. Complete the description template by checking off all verification steps and filling in the notes section to provide full context for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Claude support' directly and clearly summarizes the main objective of the pull request—introducing Claude as a second usage provider alongside Cursor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dv/claude-support

@divyanshub024

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/AIMeter/UI/SettingsWindowController.swift (1)

26-26: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Initial window content size is below SettingsView minHeight of 520.

window.setContentSize(NSSize(width: 520, height: 320)) is shorter than the SwiftUI form's minHeight: 520. The window will auto-grow on first show, but the explicit initial size is misleading. Bump it to 520x520 (or similar) to match SettingsView.

-        window.setContentSize(NSSize(width: 520, height: 320))
+        window.setContentSize(NSSize(width: 520, height: 520))
🤖 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 `@Sources/AIMeter/UI/SettingsWindowController.swift` at line 26, Update the
initial window content size to match the SettingsView minHeight by changing the
call to window.setContentSize(NSSize(width: 520, height: 520)); modify the
existing window.setContentSize(...) invocation in SettingsWindowController (the
constructor/initializer or setup method that calls window.setContentSize) so the
height is 520 to align with SettingsView's minHeight.
Sources/AIMeter/Core/CursorUsageCoordinator.swift (1)

47-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block connect() while a refresh is already in flight.

refresh() guards against isConnecting, but connect() does not guard against isRefreshing. Since both paths suspend on await, a scheduled refresh can still be running when the user clicks Connect, which lets two client operations overlap against the same provider session state.

Suggested fix
 func connect() async {
-    if isConnecting {
+    if isConnecting || isRefreshing {
         return
     }
🤖 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 `@Sources/AIMeter/Core/CursorUsageCoordinator.swift` around lines 47 - 57,
connect() can run concurrently with an in-flight refresh because it only checks
isConnecting; update connect() to also guard against isRefreshing (e.g. guard
!isConnecting && !isRefreshing { return } or return early if isRefreshing)
before setting isConnecting, so client.connect() and fetchAndStoreSnapshot()
cannot overlap with refresh(); reference the connect() method, the isConnecting
and isRefreshing flags, and the client.connect()/fetchAndStoreSnapshot() calls
when making the change.
🧹 Nitpick comments (6)
Sources/AIMeter/Core/LaunchAtLoginController.swift (1)

86-88: 💤 Low value

Unnecessary break in a switch case.

Swift switch cases don't fall through, so the break on line 87 is dead code and can be removed.

🧹 Proposed fix
 case .enabled, .requiresApproval:
     userDefaults.set(true, forKey: defaultAppliedKey)
-    break
 }
🤖 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 `@Sources/AIMeter/Core/LaunchAtLoginController.swift` around lines 86 - 88,
Remove the redundant `break` inside the switch case in LaunchAtLoginController
where `userDefaults.set(true, forKey: defaultAppliedKey)` is called — Swift
switch cases do not fall through, so delete the `break` statement (search for
the `userDefaults.set(true, forKey: defaultAppliedKey)` line or the switch
inside LaunchAtLoginController to locate the exact case).
scripts/run_dev.sh (1)

84-84: 💤 Low value

App name is hardcoded in osascript call instead of using $SCHEME.

If SCHEME is ever renamed or reused for a different target, the graceful quit won't find the process by its display name.

🛠️ Proposed fix
-  osascript -e 'tell application "AIMeter" to quit' >/dev/null 2>&1 || true
+  osascript -e "tell application \"$SCHEME\" to quit" >/dev/null 2>&1 || true
🤖 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 `@scripts/run_dev.sh` at line 84, The osascript call uses a hardcoded app name
"AIMeter" instead of the SCHEME variable; update the command that currently
calls osascript -e 'tell application "AIMeter" to quit' to interpolate the
SCHEME variable (e.g., use the quoted/escaped value of $SCHEME) so the script
quits the app matching the current SCHEME; ensure proper quoting/escaping of
$SCHEME in the osascript argument and preserve the >/dev/null 2>&1 || true
behavior.
Sources/AIMeter/UI/MenuBarController.swift (3)

91-104: 💤 Low value

Resizing popover.contentSize while shown may cause visible jumps.

updateStatusItem is invoked on every DashboardState change. If a snapshot transitions a provider from disconnected to connected (or vice versa) while the popover is open, preferredPopoverSize(for:) will switch heights between 370/420/560 and the user will see the popover snap. Consider only updating contentSize when the popover is not shown, or guarding with a "shape" key derived from (presentationState, connectedProviderCount) so it only changes on actual category transitions.

🤖 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 `@Sources/AIMeter/UI/MenuBarController.swift` around lines 91 - 104,
updateStatusItem currently sets popover.contentSize on every DashboardState
change causing visible jumps when the popover is open; change it to only assign
popover.contentSize when the popover is not shown (check popover.isShown) or
when a derived "shape" key changes (compute a key from presentationState and
connectedProviderCount) — store the lastShape on the MenuBarController and
update contentSize only if newShape != lastShape, otherwise skip modifying
popover.contentSize; keep button image/title/toolTip updates as-is.

118-133: 💤 Low value

Hardcoded provider names in empty-state tooltip.

"AIMeter: Connect Cursor or Claude" is hardcoded; if you add another provider this string will silently go stale. Deriving the list from UsageProvider.allCases (using displayName) keeps it in sync.

🤖 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 `@Sources/AIMeter/UI/MenuBarController.swift` around lines 118 - 133, The
tooltip(for state: DashboardState) currently returns a hardcoded empty-state
string "AIMeter: Connect Cursor or Claude"; change it to build the provider list
dynamically from UsageProvider.allCases using each provider's displayName so new
providers stay in sync. Locate tooltip(for:) and replace the hardcoded branch
that returns the static string when connectedProviderSnapshots is empty with
logic that maps UsageProvider.allCases.map(\.displayName).joined(separator: " or
") (or similar formatting) to produce the message (e.g., "AIMeter: Connect
<providers>"), ensuring you still use the existing connectedProviderSnapshots,
DashboardState, and displayName symbols.

220-273: 💤 Low value

Rename StatusBarImageFactory parameters to use ProviderConnectionState instead of the CursorConnectionState alias.

Since CursorConnectionState is a typealias for ProviderConnectionState and the popover now handles multiple providers, using the generic type name in image(...), drawIndicator(...), and indicatorColor(for:) improves clarity and prevents naming from suggesting cursor-specific behavior.

🤖 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 `@Sources/AIMeter/UI/MenuBarController.swift` around lines 220 - 273, Change
the type names used in StatusBarImageFactory to the more generic
ProviderConnectionState: update the signature of image(progress:state:) to
accept state: ProviderConnectionState, update drawIndicator(atX:state:) and
indicatorColor(for:) to take ProviderConnectionState, and update all internal
uses and switch cases to refer to ProviderConnectionState variants (instead of
CursorConnectionState) so the factory reflects provider-wide connection states.
Tests/AIMeterTests/CursorUsageCoordinatorTests.swift (1)

149-149: 💤 Low value

Prefer the provider-generic error type in Claude coordinator test.

CursorUsageError is a typealias for ProviderUsageError. In a test explicitly driving a Claude coordinator, use ProviderUsageError.syncFailed("Claude layout changed") for naming consistency.

🤖 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 `@Tests/AIMeterTests/CursorUsageCoordinatorTests.swift` at line 149, The test
currently asserts a failure with the provider-specific alias CursorUsageError;
change it to use the underlying provider-generic type ProviderUsageError so
naming matches the Claude coordinator context—replace
.failure(CursorUsageError.syncFailed("Claude layout changed")) with
.failure(ProviderUsageError.syncFailed("Claude layout changed")) in the
CursorUsageCoordinatorTests.swift test case.
🤖 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 `@scripts/build_verify_notarized_dmg.sh`:
- Around line 8-9: Remove the hardcoded Apple Team ID default in the script by
no longer setting DEVELOPMENT_TEAM to "W2B7PMH9SQ" (and similarly avoid
embedding a default for NOTARYTOOL_PROFILE if desired); instead require callers
to provide DEVELOPMENT_TEAM via environment and add a fail-fast check in the
script (e.g., at script start) that validates DEVELOPMENT_TEAM is set and exits
with a clear error message if not. Update the export lines that reference
DEVELOPMENT_TEAM and NOTARYTOOL_PROFILE so they do not assign the hardcoded
defaults and add a small validation block in build_verify_notarized_dmg.sh that
checks the DEVELOPMENT_TEAM (and optionally NOTARYTOOL_PROFILE) environment
variable(s) and aborts with a descriptive error to prevent accidental
notarization attempts under the wrong account.

In `@Sources/AIMeter/App/AppDelegate.swift`:
- Around line 11-16: applicationWillTerminate currently launches an async Task
which may not run before process exit; replace that with synchronous Main-thread
calls so both stop() methods run reliably: remove Task { `@MainActor` in ... } and
call AppEnvironment.shared.cursorUsageCoordinator.stop() and
AppEnvironment.shared.claudeUsageCoordinator.stop() inside a
DispatchQueue.main.sync (or equivalent MainActor-synchronous) block so the
`@MainActor-annotated` stop() methods execute synchronously before termination.

In `@Sources/AIMeter/Claude/ClaudeSessionManager.swift`:
- Around line 64-78: clearWebsiteData() runs its removeData call asynchronously
and can race with immediate reconnects in connect(to:) / fetchUsage(from:);
change clearWebsiteData to provide an async completion (or return asynchronously
via withCheckedContinuation) and await it from disconnect(), or alternatively
add a serializing flag (e.g. isCleaningWebsiteData) that clearWebsiteData
sets/clears and that connect(to:)/fetchUsage(from:) check/wait on before reusing
dataStore; specifically modify clearWebsiteData(), disconnect(), and the callers
connect(to:)/fetchUsage(from:) to either await the dataStore.removeData
completion or block new connects until cleanup finishes so the removeData
callback cannot delete a newly created session.

In `@Sources/AIMeter/Claude/ClaudeURLValidator.swift`:
- Around line 47-65: Update deprecated URL property accesses to their
percentEncoded: variants: replace uses of url.user, url.password, url.host, and
url.path with url.user(percentEncoded:), url.password(percentEncoded:),
url.host(percentEncoded:) and url.path(percentEncoded:) respectively in
ClaudeURLValidator.swift (notably inside isAllowedClaudeURL(_:),
sanitizedUsageURL, and isUsageSettingsURLString) and adapt the logic to safely
unwrap the percent-encoded strings (e.g., lowercasing host after unwrapping) to
preserve existing behavior while avoiding macOS 14 compiler deprecation
warnings.
- Around line 23-65: Replace deprecated direct URL property accesses with the
percentEncoded variants to silence macOS 14 warnings: use
url.path(percentEncoded: false) wherever url.path is read (including the earlier
method that returns usagePageURL), use url.user(percentEncoded: false) and
url.password(percentEncoded: false) in the credential checks, and use
url.host(percentEncoded: false) when normalizing the host in
isAllowedClaudeURL(_:) and isUsageSettingsURLString(_:); keep the same logic
(lowercased/trimming/ comparisons) but call the percentEncoded: false accessors
in isAllowedClaudeURLString(_:), isUsageSettingsURLString(_:), and
isAllowedClaudeURL(_:).

In `@Sources/AIMeter/Core/CursorUsageClient.swift`:
- Around line 30-32: The .cancelled case in ProviderUsageError currently returns
a hardcoded "Cursor" string; update the message in the ProviderUsageError
.cancelled case (in CursorUsageClient.swift) to avoid referencing Cursor
directly and instead use a generic provider placeholder or the actual provider
identifier (e.g., use "\(providerName)" or "the provider") so it reads like
"Connection window closed before provider finished loading." Ensure you
reference the provider name variable used in the surrounding context (or fall
back to a generic "provider") when composing the returned error description.
- Around line 31-32: The .cancelled error case in CursorUsageClient.swift
currently returns a Cursor-specific message ("Connection window closed before
Cursor finished loading.") which is provider-specific; update the .cancelled
error description to a provider-agnostic message (e.g., "Connection window
closed before the operation finished." or "Connection window closed before
completion.") inside the same error/description implementation so any provider
(Claude or others) shows a neutral message; keep the change localized to the
.cancelled return value and preserve the existing error type and surrounding
structure.

In `@Sources/AIMeter/Core/Models.swift`:
- Around line 253-255: The computed property connectedProviderSnapshots
currently filters providerSnapshots by requiring connectionState == .connected,
which incorrectly hides snapshots with .authExpired and .syncFailed; change the
predicate in connectedProviderSnapshots to only exclude .disconnected (e.g.,
filter { $0.connectionState != .disconnected }) so preserved snapshots with
error/reconnect states remain visible; update the ProviderUsageSnapshot usage to
reference connectionState when applying this new filter.

In `@Sources/AIMeter/UI/MenuBarController.swift`:
- Around line 135-144: primaryConnectionState(for:) currently only returns
.connected or .disconnected, dropping .authExpired and .syncFailed; update the
function to scan state.connectedProviderSnapshots and preserve the worst
non-connected state: if any snapshot.connectionState == .authExpired return
.authExpired; else if any == .syncFailed return .syncFailed; else if any
snapshot.connectionState == .connected && snapshot.progressPercent != nil return
.connected; otherwise return .disconnected. Use the existing
connectedProviderSnapshots and connectionState checks to implement this order so
the status bar can surface auth/sync failures.

---

Outside diff comments:
In `@Sources/AIMeter/Core/CursorUsageCoordinator.swift`:
- Around line 47-57: connect() can run concurrently with an in-flight refresh
because it only checks isConnecting; update connect() to also guard against
isRefreshing (e.g. guard !isConnecting && !isRefreshing { return } or return
early if isRefreshing) before setting isConnecting, so client.connect() and
fetchAndStoreSnapshot() cannot overlap with refresh(); reference the connect()
method, the isConnecting and isRefreshing flags, and the
client.connect()/fetchAndStoreSnapshot() calls when making the change.

In `@Sources/AIMeter/UI/SettingsWindowController.swift`:
- Line 26: Update the initial window content size to match the SettingsView
minHeight by changing the call to window.setContentSize(NSSize(width: 520,
height: 520)); modify the existing window.setContentSize(...) invocation in
SettingsWindowController (the constructor/initializer or setup method that calls
window.setContentSize) so the height is 520 to align with SettingsView's
minHeight.

---

Nitpick comments:
In `@scripts/run_dev.sh`:
- Line 84: The osascript call uses a hardcoded app name "AIMeter" instead of the
SCHEME variable; update the command that currently calls osascript -e 'tell
application "AIMeter" to quit' to interpolate the SCHEME variable (e.g., use the
quoted/escaped value of $SCHEME) so the script quits the app matching the
current SCHEME; ensure proper quoting/escaping of $SCHEME in the osascript
argument and preserve the >/dev/null 2>&1 || true behavior.

In `@Sources/AIMeter/Core/LaunchAtLoginController.swift`:
- Around line 86-88: Remove the redundant `break` inside the switch case in
LaunchAtLoginController where `userDefaults.set(true, forKey:
defaultAppliedKey)` is called — Swift switch cases do not fall through, so
delete the `break` statement (search for the `userDefaults.set(true, forKey:
defaultAppliedKey)` line or the switch inside LaunchAtLoginController to locate
the exact case).

In `@Sources/AIMeter/UI/MenuBarController.swift`:
- Around line 91-104: updateStatusItem currently sets popover.contentSize on
every DashboardState change causing visible jumps when the popover is open;
change it to only assign popover.contentSize when the popover is not shown
(check popover.isShown) or when a derived "shape" key changes (compute a key
from presentationState and connectedProviderCount) — store the lastShape on the
MenuBarController and update contentSize only if newShape != lastShape,
otherwise skip modifying popover.contentSize; keep button image/title/toolTip
updates as-is.
- Around line 118-133: The tooltip(for state: DashboardState) currently returns
a hardcoded empty-state string "AIMeter: Connect Cursor or Claude"; change it to
build the provider list dynamically from UsageProvider.allCases using each
provider's displayName so new providers stay in sync. Locate tooltip(for:) and
replace the hardcoded branch that returns the static string when
connectedProviderSnapshots is empty with logic that maps
UsageProvider.allCases.map(\.displayName).joined(separator: " or ") (or similar
formatting) to produce the message (e.g., "AIMeter: Connect <providers>"),
ensuring you still use the existing connectedProviderSnapshots, DashboardState,
and displayName symbols.
- Around line 220-273: Change the type names used in StatusBarImageFactory to
the more generic ProviderConnectionState: update the signature of
image(progress:state:) to accept state: ProviderConnectionState, update
drawIndicator(atX:state:) and indicatorColor(for:) to take
ProviderConnectionState, and update all internal uses and switch cases to refer
to ProviderConnectionState variants (instead of CursorConnectionState) so the
factory reflects provider-wide connection states.

In `@Tests/AIMeterTests/CursorUsageCoordinatorTests.swift`:
- Line 149: The test currently asserts a failure with the provider-specific
alias CursorUsageError; change it to use the underlying provider-generic type
ProviderUsageError so naming matches the Claude coordinator context—replace
.failure(CursorUsageError.syncFailed("Claude layout changed")) with
.failure(ProviderUsageError.syncFailed("Claude layout changed")) in the
CursorUsageCoordinatorTests.swift test case.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a7edf32-0b49-4c19-ba79-d9eef9f868b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4283b7a and e52ac2f.

📒 Files selected for processing (30)
  • AIMeter.xcodeproj/project.pbxproj
  • CHANGELOG.md
  • Sources/AIMeter/App/AIMeterApp.swift
  • Sources/AIMeter/App/AppDelegate.swift
  • Sources/AIMeter/App/AppEnvironment.swift
  • Sources/AIMeter/Claude/ClaudeDashboardClient.swift
  • Sources/AIMeter/Claude/ClaudeDashboardParser.swift
  • Sources/AIMeter/Claude/ClaudeSessionManager.swift
  • Sources/AIMeter/Claude/ClaudeURLValidator.swift
  • Sources/AIMeter/Claude/ClaudeWebViewScraper.swift
  • Sources/AIMeter/Core/CursorUsageClient.swift
  • Sources/AIMeter/Core/CursorUsageCoordinator.swift
  • Sources/AIMeter/Core/DashboardStore.swift
  • Sources/AIMeter/Core/LaunchAtLoginController.swift
  • Sources/AIMeter/Core/Models.swift
  • Sources/AIMeter/Cursor/CursorDashboardClient.swift
  • Sources/AIMeter/Storage/SettingsStore.swift
  • Sources/AIMeter/UI/MenuBarController.swift
  • Sources/AIMeter/UI/MenuPopoverView.swift
  • Sources/AIMeter/UI/SettingsView.swift
  • Sources/AIMeter/UI/SettingsWindowController.swift
  • Tests/AIMeterTests/ClaudeDashboardClientTests.swift
  • Tests/AIMeterTests/ClaudeDashboardParserTests.swift
  • Tests/AIMeterTests/ClaudeURLValidatorTests.swift
  • Tests/AIMeterTests/CursorUsageCoordinatorTests.swift
  • Tests/AIMeterTests/DashboardStoreTests.swift
  • Tests/AIMeterTests/LaunchAtLoginControllerTests.swift
  • Tests/AIMeterTests/SettingsStoreTests.swift
  • scripts/build_verify_notarized_dmg.sh
  • scripts/run_dev.sh

Comment thread scripts/build_verify_notarized_dmg.sh Outdated
Comment thread Sources/AIMeter/App/AppDelegate.swift
Comment thread Sources/AIMeter/Claude/ClaudeSessionManager.swift
Comment thread Sources/AIMeter/Claude/ClaudeURLValidator.swift Outdated
Comment thread Sources/AIMeter/Claude/ClaudeURLValidator.swift
Comment thread Sources/AIMeter/Core/CursorUsageClient.swift
Comment thread Sources/AIMeter/Core/CursorUsageClient.swift Outdated
Comment thread Sources/AIMeter/Core/Models.swift
Comment thread Sources/AIMeter/UI/MenuBarController.swift
Remove the scripts directory from Git tracking while keeping the files available locally.

Ignore scripts/ so build, release, icon, and development helper scripts remain local-only going forward.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Tests/AIMeterTests/DashboardStoreTests.swift (1)

241-243: ⚡ Quick win

Guard mock fetch queue to avoid trap-based test failures.

Line 242 uses removeFirst() directly; if a test refreshes more than expected, this crashes with an index trap instead of producing a clear assertion failure.

Proposed test-harness hardening
 func fetchUsage() async throws -> CursorUsageSnapshot {
-    let result = fetchResults.removeFirst()
+    XCTAssertFalse(fetchResults.isEmpty, "DashboardMockCursorUsageClient.fetchResults exhausted before fetchUsage()")
+    let result = fetchResults.removeFirst()
     return try result.get()
 }
🤖 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 `@Tests/AIMeterTests/DashboardStoreTests.swift` around lines 241 - 243, The
test harness currently uses fetchResults.removeFirst() in fetchUsage(), which
will crash with an index trap when the mock queue is empty; instead guard the
queue: check fetchResults.isEmpty at the start of fetchUsage(), call a test
failure (e.g., XCTFail with a clear message) and then throw a descriptive test
error (or return a failed Result) so the test fails cleanly; update the
fetchUsage() implementation and any test helper types to include a TestError (or
reuse an existing test error type) and reference fetchResults and fetchUsage()
when making this change.
Sources/AIMeter/Claude/ClaudeSessionManager.swift (1)

48-55: 💤 Low value

Unnecessary do-catch that only rethrows.

The catch { throw error } block adds no handling. Remove it and let the error propagate naturally.

♻️ Proposed simplification
-        do {
-            let snapshot = try await scraper.start()
-            isConnected = true
-            windowController.close()
-            return snapshot
-        } catch {
-            throw error
-        }
+        let snapshot = try await scraper.start()
+        isConnected = true
+        return snapshot
🤖 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 `@Sources/AIMeter/Claude/ClaudeSessionManager.swift` around lines 48 - 55, The
do-catch around calling scraper.start() is redundant because the catch only
rethrows; remove the entire do-catch and await scraper.start() directly, set
isConnected = true and call windowController.close() then return the snapshot
(i.e. replace the do { let snapshot = try await scraper.start(); isConnected =
true; windowController.close(); return snapshot } catch { throw error } with a
direct try await call and the same subsequent lines), keeping function signature
and error propagation unchanged.
🤖 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 `@Sources/AIMeter/Claude/ClaudeSessionManager.swift`:
- Around line 43-55: The connection window isn't closed when scraper.start()
throws because windowController.close() is only called on the success path; move
the dismissal into the defer so it always runs: in ClaudeSessionManager.swift
ensure the defer block that currently nils activeScraper and
connectionWindowController also calls windowController.close(), so that
regardless of whether await scraper.start() succeeds or throws (non-cancellation
errors or timeouts), the "Connect Claude" sheet is dismissed and resources are
cleaned up; reference activeScraper, connectionWindowController,
windowController and the await scraper.start() call when making the change.

In `@Sources/AIMeter/UI/MenuBarController.swift`:
- Line 94: The popover is being resized on every state update causing a visible
jump; update the assignment of popover.contentSize in MenuBarController so it
only applies the value from preferredPopoverSize(for:) when the popover is not
currently shown (check the NSPopover isShown state) and skip resizing while
popover.isShown is true, keeping the current size until the popover is closed.

---

Nitpick comments:
In `@Sources/AIMeter/Claude/ClaudeSessionManager.swift`:
- Around line 48-55: The do-catch around calling scraper.start() is redundant
because the catch only rethrows; remove the entire do-catch and await
scraper.start() directly, set isConnected = true and call
windowController.close() then return the snapshot (i.e. replace the do { let
snapshot = try await scraper.start(); isConnected = true;
windowController.close(); return snapshot } catch { throw error } with a direct
try await call and the same subsequent lines), keeping function signature and
error propagation unchanged.

In `@Tests/AIMeterTests/DashboardStoreTests.swift`:
- Around line 241-243: The test harness currently uses
fetchResults.removeFirst() in fetchUsage(), which will crash with an index trap
when the mock queue is empty; instead guard the queue: check
fetchResults.isEmpty at the start of fetchUsage(), call a test failure (e.g.,
XCTFail with a clear message) and then throw a descriptive test error (or return
a failed Result) so the test fails cleanly; update the fetchUsage()
implementation and any test helper types to include a TestError (or reuse an
existing test error type) and reference fetchResults and fetchUsage() when
making this change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 996f5fa9-edec-4691-90b9-ca54ab5d59e7

📥 Commits

Reviewing files that changed from the base of the PR and between e52ac2f and adefe2f.

📒 Files selected for processing (12)
  • .gitignore
  • Sources/AIMeter/App/AppDelegate.swift
  • Sources/AIMeter/Claude/ClaudeSessionManager.swift
  • Sources/AIMeter/Claude/ClaudeURLValidator.swift
  • Sources/AIMeter/Core/CursorUsageClient.swift
  • Sources/AIMeter/Core/Models.swift
  • Sources/AIMeter/UI/MenuBarController.swift
  • Tests/AIMeterTests/DashboardStoreTests.swift
  • scripts/build_dmg.sh
  • scripts/generate_app_icon.swift
  • scripts/generate_dmg_background.swift
  • scripts/release_github.sh
💤 Files with no reviewable changes (4)
  • scripts/release_github.sh
  • scripts/generate_dmg_background.swift
  • scripts/generate_app_icon.swift
  • scripts/build_dmg.sh
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • Sources/AIMeter/Core/Models.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/AIMeter/App/AppDelegate.swift
  • Sources/AIMeter/Claude/ClaudeURLValidator.swift

Comment thread Sources/AIMeter/Claude/ClaudeSessionManager.swift Outdated
private func updateStatusItem(_ state: DashboardState) {
guard let button = statusItem.button else { return }

popover.contentSize = preferredPopoverSize(for: state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Popover resizes while open, causing a visible size jump.

preferredPopoverSize is applied on every state update, including while the popover is visible. Transitioning between 1 and 2 connected providers changes height by 190 pt in-place. Consider only applying the new size when the popover is not shown.

🔧 Suggested guard
-        popover.contentSize = preferredPopoverSize(for: state)
+        if !popover.isShown {
+            popover.contentSize = preferredPopoverSize(for: state)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
popover.contentSize = preferredPopoverSize(for: state)
if !popover.isShown {
popover.contentSize = preferredPopoverSize(for: state)
}
🤖 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 `@Sources/AIMeter/UI/MenuBarController.swift` at line 94, The popover is being
resized on every state update causing a visible jump; update the assignment of
popover.contentSize in MenuBarController so it only applies the value from
preferredPopoverSize(for:) when the popover is not currently shown (check the
NSPopover isShown state) and skip resizing while popover.isShown is true,
keeping the current size until the popover is closed.

Move Claude connection window dismissal into the connect defer path so scraper errors, cancellations, and timeouts clean up the visible window and controller references.
@divyanshub024
divyanshub024 merged commit 2e0ff90 into main May 7, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant