Skip to content

Display Cursor Auto & API percentages on menu bar#2

Open
acceleroto wants to merge 9 commits into
divyanshub024:mainfrom
acceleroto:main
Open

Display Cursor Auto & API percentages on menu bar#2
acceleroto wants to merge 9 commits into
divyanshub024:mainfrom
acceleroto:main

Conversation

@acceleroto

@acceleroto acceleroto commented May 24, 2026

Copy link
Copy Markdown

Summary

Adds an optional menu bar label that shows Cursor Auto and API usage (e.g. 5.6%/7.8%) beside the existing progress bar, controlled by a setting in the Cursor section of Settings.

Changes

  • Introduce MenuBarAppearanceSettings on AppSettings (persisted via SettingsStore) with showCursorAutoAPIPercentages (default off); showProgressBar remains in the model and is always merged to true for now.
  • Render the Auto/API suffix to the right of the status item image when enabled and Cursor has synced usage data; use plain button.title so text adapts in light/dark menu bars.
  • Add Show Cursor Auto & API percentages in Menu Bar toggle under the Cursor connect/disconnect controls in Settings.
  • Add DisplayFormatting.compactPercent and cursorAutoAPISuffix(auto:api:) for consistent formatting.
  • Extend menu bar tooltip with Cursor Auto/API lines when the option is enabled.
  • Add unit tests for formatting and menu bar settings persistence/migration.
  • Document the feature in README.md.

Verification

  • xcodegen generate
  • xcodebuild -project AIMeter.xcodeproj -scheme AIMeter -destination "platform=macOS" -derivedDataPath ./.derived build
  • xcodebuild -project AIMeter.xcodeproj -scheme AIMeter -destination "platform=macOS" -derivedDataPath ./.derived test
  • README, screenshots, or changelog updated when user-visible behavior changed

Notes

  • The progress bar still reflects the maximum primary usage % across connected providers; the new text is Cursor Auto/API only (Claude is unchanged in the menu bar suffix). A future PR could add a similar Claude option, if desired.
  • Suffix is hidden when Cursor is disconnected or has never synced; it can still appear when Cursor is in authExpired / syncFailed if a previous sync is cached (same data as the popover).

Summary by CodeRabbit

Release Notes

  • New Features
    • Added OpenAI usage tracking (connect/disconnect) with plan + usage metrics and OpenAI Codex percentages in the menu bar.
    • Added menu bar customization toggles for the progress bar, Cursor Auto/API, and OpenAI Codex.
  • Changed
    • Menu bar rendering now combines Cursor and OpenAI segments (including improved “reset” displays) with sensible placeholders until sync completes.
    • Updated onboarding and documentation for OpenAI, plus richer reset parsing behavior.
  • Tests
    • Expanded coverage for formatting, menu bar display, OpenAI parsing, and URL validation.

@acceleroto

Copy link
Copy Markdown
Author

Screenshot of the updated menu bar showing the Cursor Auto & API percentages.
Screenshot 2026-05-24 at 2 51 27 AM

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds OpenAI usage tracking, expands menu bar display controls, extends Cursor billing reset parsing, and wires the new provider through app lifecycle, UI, tests, and project configuration.

Changes

OpenAI Tracking and Multi-Provider Menu Bar

Layer / File(s) Summary
Project and docs updates
AIMeter.xcodeproj/project.pbxproj, .gitignore, README.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, SUPPORT.md, .github/ISSUE_TEMPLATE/bug_report.md, docs/release-checklist.md
Project references, build phases, ignore rules, docs, changelog, security/support guidance, and issue template fields were updated for OpenAI, menu bar display changes, and Cursor billing/dashboard wording.
Core models, formatting, and menu bar resolver
Sources/AIMeter/Core/Models.swift, Sources/AIMeter/Core/DisplayFormatting.swift, Tests/AIMeterTests/DisplayFormattingTests.swift, Tests/AIMeterTests/MenuBarDisplayTests.swift
Shared settings and snapshot models now include OpenAI and menu-bar flags, display formatting adds suffix and reset parsing helpers, and the menu bar resolver combines Cursor and OpenAI segments with matching tests for formatting and resolver behavior.
Cursor billing reset parsing and scraper flow
Sources/AIMeter/Cursor/CursorDashboardParser.swift, Sources/AIMeter/Cursor/CursorURLValidator.swift, Sources/AIMeter/Cursor/CursorWebViewScraper.swift, Tests/AIMeterTests/CursorDashboardParserTests.swift, Tests/AIMeterTests/CursorURLValidatorTests.swift
Cursor parsing now extracts billing reset text and dates, Cursor URL validation separates normal and response hosts, the scraper switches to a usage-then-billing flow with fallback handling, and tests cover the new parsing and host rules.
OpenAI URL validation, parsing, and WebKit ingestion
Sources/AIMeter/OpenAI/OpenAIDashboardClient.swift, Sources/AIMeter/OpenAI/OpenAIDashboardParser.swift, Sources/AIMeter/OpenAI/OpenAISessionManager.swift, Sources/AIMeter/OpenAI/OpenAIWebViewScraper.swift, Tests/AIMeterTests/OpenAIDashboardParserTests.swift, Tests/AIMeterTests/OpenAIURLValidatorTests.swift
OpenAI now has a validated usage URL, a dashboard parser for DOM/JSON content, a session manager that drives WebKit scraping and cleanup, and a provider client that connects and fetches usage snapshots.
App composition and multi-provider UI
Sources/AIMeter/App/AppEnvironment.swift, Sources/AIMeter/App/AppDelegate.swift, Sources/AIMeter/App/AIMeterApp.swift, Sources/AIMeter/Core/CursorUsageCoordinator.swift, Sources/AIMeter/Core/DashboardStore.swift, Sources/AIMeter/UI/MenuBarController.swift, Sources/AIMeter/UI/MenuPopoverView.swift, Sources/AIMeter/UI/SettingsWindowController.swift, Sources/AIMeter/UI/SettingsView.swift, Sources/AIMeter/UI/UsageMetricCardsRow.swift, Tests/AIMeterTests/SettingsStoreTests.swift
The OpenAI coordinator is threaded through app composition, the menu bar and popover react to settings and provider state, the settings view exposes the new toggles, and the new metric card row is used for provider usage cards. Menu bar display tests cover the new combinations.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • divyanshub024/aimeter#1: It covers the earlier multi-provider wiring pattern in the same app areas, which this PR extends to OpenAI.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main user-facing change: showing Cursor Auto/API percentages in the menu bar.
Description check ✅ Passed The description follows the required template and includes complete Summary, Changes, Verification, and Notes sections.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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.

🧹 Nitpick comments (1)
Tests/AIMeterTests/DisplayFormattingTests.swift (1)

5-18: ⚡ Quick win

Add boundary-format tests for clamp and threshold behavior.

compactPercent now powers both standalone and suffix formatting. Add a couple of boundary assertions to lock 0...100 clamping and near-integer behavior.

➕ Proposed test additions
 final class DisplayFormattingTests: XCTestCase {
@@
     func testCursorAutoAPISuffixJoinsCompactPercents() {
         XCTAssertEqual(DisplayFormatting.cursorAutoAPISuffix(auto: 5.6, api: 7.8), "5.6%/7.8%")
         XCTAssertEqual(DisplayFormatting.cursorAutoAPISuffix(auto: 6, api: 8), "6%/8%")
     }
+
+    func testCompactPercentClampsOutOfRangeValues() {
+        XCTAssertEqual(DisplayFormatting.compactPercent(-3.2), "0%")
+        XCTAssertEqual(DisplayFormatting.compactPercent(123.4), "100%")
+    }
+
+    func testCompactPercentNearIntegerThresholdFormatting() {
+        XCTAssertEqual(DisplayFormatting.compactPercent(5.96), "6%")
+    }
 }
🤖 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/DisplayFormattingTests.swift` around lines 5 - 18, Add
boundary tests to assert compactPercent clamps values to 0 and 100 and preserves
near-integer formatting for values at the edges; specifically, extend
Tests/AIMeterTests/DisplayFormattingTests.swift to include assertions like
XCTAssertEqual(DisplayFormatting.compactPercent(-1), "0%") and
XCTAssertEqual(DisplayFormatting.compactPercent(101), "100%") to lock clamping,
plus near-integer checks such as
XCTAssertEqual(DisplayFormatting.compactPercent(0.4), "0.4%") and
XCTAssertEqual(DisplayFormatting.compactPercent(99.6), "99.6%"); also add
matching cursorAutoAPISuffix assertions using
DisplayFormatting.cursorAutoAPISuffix(auto: ..., api: ...) to ensure suffix
formatting follows the same clamping and near-integer rules.
🤖 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.

Nitpick comments:
In `@Tests/AIMeterTests/DisplayFormattingTests.swift`:
- Around line 5-18: Add boundary tests to assert compactPercent clamps values to
0 and 100 and preserves near-integer formatting for values at the edges;
specifically, extend Tests/AIMeterTests/DisplayFormattingTests.swift to include
assertions like XCTAssertEqual(DisplayFormatting.compactPercent(-1), "0%") and
XCTAssertEqual(DisplayFormatting.compactPercent(101), "100%") to lock clamping,
plus near-integer checks such as
XCTAssertEqual(DisplayFormatting.compactPercent(0.4), "0.4%") and
XCTAssertEqual(DisplayFormatting.compactPercent(99.6), "99.6%"); also add
matching cursorAutoAPISuffix assertions using
DisplayFormatting.cursorAutoAPISuffix(auto: ..., api: ...) to ensure suffix
formatting follows the same clamping and near-integer rules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d20f7e0-b59f-4f95-9aa2-c571e5f55583

📥 Commits

Reviewing files that changed from the base of the PR and between ba36f6b and d338775.

📒 Files selected for processing (11)
  • .cursor/plans/menu_bar_appearance_toggles_080147a0.plan.md
  • AIMeter.xcodeproj/project.pbxproj
  • README.md
  • Sources/AIMeter/App/AppEnvironment.swift
  • Sources/AIMeter/Core/DisplayFormatting.swift
  • Sources/AIMeter/Core/Models.swift
  • Sources/AIMeter/Storage/SettingsStore.swift
  • Sources/AIMeter/UI/MenuBarController.swift
  • Sources/AIMeter/UI/SettingsView.swift
  • Tests/AIMeterTests/DisplayFormattingTests.swift
  • Tests/AIMeterTests/SettingsStoreTests.swift

@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: 5

🧹 Nitpick comments (2)
README.md (1)

14-16: 💤 Low value

Refactor repeated "Tracks" to improve readability.

Lines 14–16 all begin with "Tracks," creating a stylistic repetition. Consider rewording one or two bullets for variety:

  • "Tracks Cursor total, Auto, and API usage."
  • "Tracks Claude plan usage, reset time, and model limits when available."
  • "OpenAI ChatGPT Plus Codex usage tracking: 5-hour limit, weekly limit, and credits from the Codex analytics page."

Or use a synonym like "Monitors" or "Displays" for OpenAI to add variety while maintaining clarity.

🤖 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 `@README.md` around lines 14 - 16, The three consecutive bullet points
describing Cursor, Claude, and OpenAI usage all begin with "Tracks," creating
repetitive phrasing that reduces readability. Refactor at least one or two of
these bullet points to use alternative verbs such as "Monitors" or "Displays,"
or restructure them entirely (for example, changing the OpenAI bullet to "OpenAI
ChatGPT Plus Codex usage tracking:" or "Monitors OpenAI ChatGPT Plus Codex
usage") to introduce variety while maintaining clear and descriptive content.
Keep the core information about what is being tracked the same, only modify the
introductory phrasing.

Source: Linters/SAST tools

Tests/AIMeterTests/MenuBarDisplayTests.swift (1)

73-104: ⚡ Quick win

Add a mixed-availability title test to prevent separator regressions.

Please add a case where both toggles are enabled, showProgressBar is true, one provider is disconnected, and the other is connected; assert no leading/trailing pipe in titleText.

🤖 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/MenuBarDisplayTests.swift` around lines 73 - 104, Add a
new test method in the MenuBarDisplayTests class that tests a mixed availability
scenario to prevent separator regressions. Create this test with both
showCursorAutoAPIPercentages and showOpenAICodexPercentages enabled, but set
showProgressBar to true (unlike the existing
testShowsCursorThenOpenAISegmentsSeparatedByPipe test). Set one of the provider
snapshots (either cursor or openAI) to have connectionState of .disconnected
while the other remains .connected. Then assert that the resulting
display.titleText does not have any leading or trailing pipe characters,
ensuring the separator logic correctly handles mixed connectivity states.
🤖 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/Core/MenuBarDisplay.swift`:
- Around line 47-58: The segments array being joined with segmentSeparator can
contain empty strings when providers are unavailable and placeholders are
disabled, resulting in dangling separators in the output. Filter out empty
segment strings before joining them in the return statement. In the section
where segments are joined with separator, add a filter condition to remove any
empty strings from the segments array before performing the joined operation, so
that only non-empty provider segments are included in the final output string.

In `@Sources/AIMeter/Cursor/CursorDashboardParser.swift`:
- Around line 378-403: The jsonStringLeaves function currently only extracts and
returns string leaves from JSON objects, but numeric epoch timestamp values are
not being collected. Modify the jsonStringLeaves function to also handle numeric
types (such as Int, Double, etc.) by converting them to strings and including
them in the returned array alongside string values. This will allow numeric
epoch fields in the billing JSON to be discovered and passed to the date parsing
logic for proper evaluation.

In `@Sources/AIMeter/OpenAI/OpenAIDashboardParser.swift`:
- Around line 232-237: The pattern matching in the percentage parsing logic does
not distinguish between "used" and "remaining/left" keywords, causing all
matched values to be inverted through the usedPercent(fromReportedRemaining:)
function. Modify the code to check which keyword was captured by the regex
pattern: if the match contains "used", return the reported value directly
without inversion; if it contains "remaining" or "left", then apply the
usedPercent(fromReportedRemaining:) conversion. This fix must be applied to all
three affected locations where percentage patterns are parsed with the
"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|remaining|left)" regex pattern.

In `@Sources/AIMeter/Storage/SettingsStore.swift`:
- Around line 60-62: The setOpenAIUsagePageURL method only trims whitespace from
the URL before persisting it, which bypasses the sanitization invariant enforced
in OpenAISettings.mergedWithDefaults. Apply the same sanitization logic used in
OpenAISettings.mergedWithDefaults to the URL string in the setOpenAIUsagePageURL
method to ensure invalid URLs are not persisted and maintain consistency across
all paths that handle this URL.

In `@Sources/AIMeter/UI/MenuBarController.swift`:
- Around line 259-266: In the statusItemClicked method, before calling
showContextMenu(), you need to close the popover to ensure it is not visible
when the context menu appears. Add a call to close or hide the popover (likely
using a method like hidePopover or similar) immediately after the
shouldShowContextMenu check and before the showContextMenu() call to maintain
consistent menu-bar interaction states.

---

Nitpick comments:
In `@README.md`:
- Around line 14-16: The three consecutive bullet points describing Cursor,
Claude, and OpenAI usage all begin with "Tracks," creating repetitive phrasing
that reduces readability. Refactor at least one or two of these bullet points to
use alternative verbs such as "Monitors" or "Displays," or restructure them
entirely (for example, changing the OpenAI bullet to "OpenAI ChatGPT Plus Codex
usage tracking:" or "Monitors OpenAI ChatGPT Plus Codex usage") to introduce
variety while maintaining clear and descriptive content. Keep the core
information about what is being tracked the same, only modify the introductory
phrasing.

In `@Tests/AIMeterTests/MenuBarDisplayTests.swift`:
- Around line 73-104: Add a new test method in the MenuBarDisplayTests class
that tests a mixed availability scenario to prevent separator regressions.
Create this test with both showCursorAutoAPIPercentages and
showOpenAICodexPercentages enabled, but set showProgressBar to true (unlike the
existing testShowsCursorThenOpenAISegmentsSeparatedByPipe test). Set one of the
provider snapshots (either cursor or openAI) to have connectionState of
.disconnected while the other remains .connected. Then assert that the resulting
display.titleText does not have any leading or trailing pipe characters,
ensuring the separator logic correctly handles mixed connectivity states.
🪄 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: f06387e2-dbbb-4dea-8381-e4702d5bcfa7

📥 Commits

Reviewing files that changed from the base of the PR and between 253be54 and 2223f58.

📒 Files selected for processing (29)
  • AIMeter.xcodeproj/project.pbxproj
  • README.md
  • Sources/AIMeter/App/AIMeterApp.swift
  • Sources/AIMeter/App/AppDelegate.swift
  • Sources/AIMeter/App/AppEnvironment.swift
  • Sources/AIMeter/Core/CursorUsageCoordinator.swift
  • Sources/AIMeter/Core/DashboardStore.swift
  • Sources/AIMeter/Core/DisplayFormatting.swift
  • Sources/AIMeter/Core/MenuBarDisplay.swift
  • Sources/AIMeter/Core/Models.swift
  • Sources/AIMeter/Cursor/CursorDashboardParser.swift
  • Sources/AIMeter/Cursor/CursorWebViewScraper.swift
  • Sources/AIMeter/OpenAI/OpenAIDashboardClient.swift
  • Sources/AIMeter/OpenAI/OpenAIDashboardParser.swift
  • Sources/AIMeter/OpenAI/OpenAISessionManager.swift
  • Sources/AIMeter/OpenAI/OpenAIURLValidator.swift
  • Sources/AIMeter/OpenAI/OpenAIWebViewScraper.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
  • Sources/AIMeter/UI/UsageMetricCardsRow.swift
  • Tests/AIMeterTests/CursorDashboardParserTests.swift
  • Tests/AIMeterTests/DisplayFormattingTests.swift
  • Tests/AIMeterTests/MenuBarDisplayTests.swift
  • Tests/AIMeterTests/OpenAIDashboardParserTests.swift
  • Tests/AIMeterTests/OpenAIURLValidatorTests.swift
  • Tests/AIMeterTests/SettingsStoreTests.swift
✅ Files skipped from review due to trivial changes (2)
  • Sources/AIMeter/Core/CursorUsageCoordinator.swift
  • Tests/AIMeterTests/OpenAIDashboardParserTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/AIMeterTests/SettingsStoreTests.swift

Comment on lines +47 to +58
var segments: [String] = []

if settings.showCursorAutoAPIPercentages {
segments.append(cursorSegment(from: cursorSnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
}

if settings.showOpenAICodexPercentages {
segments.append(openAISegment(from: openAISnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
}

return segments.joined(separator: segmentSeparator)
}

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 | 🟠 Major | ⚡ Quick win

Filter out empty provider segments before joining.

On Line 57, joining unfiltered segments can render dangling separators (" | ", " | 3%/0%", "9%/48% | ") when one/both providers are unavailable and placeholders are disabled. This is a user-visible formatting bug.

Proposed fix
     private static func resolvedTitleText(
         settings: MenuBarAppearanceSettings,
         cursorSnapshot: ProviderUsageSnapshot,
         openAISnapshot: ProviderUsageSnapshot
     ) -> String {
         var segments: [String] = []

         if settings.showCursorAutoAPIPercentages {
-            segments.append(cursorSegment(from: cursorSnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
+            let segment = cursorSegment(
+                from: cursorSnapshot,
+                showPlaceholderWhenEmpty: !settings.showProgressBar
+            )
+            if !segment.isEmpty { segments.append(segment) }
         }

         if settings.showOpenAICodexPercentages {
-            segments.append(openAISegment(from: openAISnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
+            let segment = openAISegment(
+                from: openAISnapshot,
+                showPlaceholderWhenEmpty: !settings.showProgressBar
+            )
+            if !segment.isEmpty { segments.append(segment) }
         }

         return segments.joined(separator: segmentSeparator)
     }
📝 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
var segments: [String] = []
if settings.showCursorAutoAPIPercentages {
segments.append(cursorSegment(from: cursorSnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
}
if settings.showOpenAICodexPercentages {
segments.append(openAISegment(from: openAISnapshot, showPlaceholderWhenEmpty: !settings.showProgressBar))
}
return segments.joined(separator: segmentSeparator)
}
var segments: [String] = []
if settings.showCursorAutoAPIPercentages {
let segment = cursorSegment(
from: cursorSnapshot,
showPlaceholderWhenEmpty: !settings.showProgressBar
)
if !segment.isEmpty { segments.append(segment) }
}
if settings.showOpenAICodexPercentages {
let segment = openAISegment(
from: openAISnapshot,
showPlaceholderWhenEmpty: !settings.showProgressBar
)
if !segment.isEmpty { segments.append(segment) }
}
return segments.joined(separator: segmentSeparator)
}
🤖 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/MenuBarDisplay.swift` around lines 47 - 58, The segments
array being joined with segmentSeparator can contain empty strings when
providers are unavailable and placeholders are disabled, resulting in dangling
separators in the output. Filter out empty segment strings before joining them
in the return statement. In the section where segments are joined with
separator, add a filter condition to remove any empty strings from the segments
array before performing the joined operation, so that only non-empty provider
segments are included in the final output string.

Comment thread Sources/AIMeter/Cursor/CursorDashboardParser.swift
Comment on lines +232 to +237
if let percent = DashboardParserSupport.firstMatch(
in: line,
pattern: #"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|remaining|left)"#
), line.lowercased().contains("credit"), let reported = Double(percent) {
return DisplayFormatting.percent(usedPercent(fromReportedRemaining: reported))
}

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 | 🟠 Major | ⚡ Quick win

Used-percentage branches are being inverted as if they were “remaining”.

The parser’s conversion pipeline assumes reported values are remaining capacity, but the % used/usage branches currently pass through raw used values. That flips values (e.g., 13% used becomes 87%) in both usage and credits parsing.

🔧 Proposed fix
@@
-        if let value = DashboardParserSupport.firstMatch(
+        if let value = DashboardParserSupport.firstMatch(
             in: line,
             pattern: #"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|usage)"#
         ) {
-            return Double(value).map { min(max($0, 0), 100) }
+            // Convert "used" to "remaining" for downstream usedPercent(...) conversion.
+            return Double(value).map { min(max(100 - $0, 0), 100) }
         }
@@
-        if let reported = percent(in: text, pattern: #"(?i)(?:usage|used|limit)[^%\n]{0,80}?(\d{1,3}(?:\.\d+)?)\s*%"#) {
-            return reported
+        if let reportedUsed = percent(in: text, pattern: #"(?i)(?:usage|used|limit)[^%\n]{0,80}?(\d{1,3}(?:\.\d+)?)\s*%"#) {
+            // Convert "used" to "remaining" for downstream usedPercent(...) conversion.
+            return min(max(100 - reportedUsed, 0), 100)
         }
@@
-        if let percent = DashboardParserSupport.firstMatch(
+        if let used = DashboardParserSupport.firstMatch(
+            in: line,
+            pattern: #"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|usage)"#
+        ), line.lowercased().contains("credit"), let value = Double(used) {
+            return DisplayFormatting.percent(min(max(value, 0), 100))
+        }
+
+        if let remaining = DashboardParserSupport.firstMatch(
             in: line,
-            pattern: #"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|remaining|left)"#
-        ), line.lowercased().contains("credit"), let reported = Double(percent) {
+            pattern: #"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:remaining|left)"#
+        ), line.lowercased().contains("credit"), let reported = Double(remaining) {
             return DisplayFormatting.percent(usedPercent(fromReportedRemaining: reported))
         }

Also applies to: 264-269, 389-395

🤖 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/OpenAI/OpenAIDashboardParser.swift` around lines 232 - 237,
The pattern matching in the percentage parsing logic does not distinguish
between "used" and "remaining/left" keywords, causing all matched values to be
inverted through the usedPercent(fromReportedRemaining:) function. Modify the
code to check which keyword was captured by the regex pattern: if the match
contains "used", return the reported value directly without inversion; if it
contains "remaining" or "left", then apply the
usedPercent(fromReportedRemaining:) conversion. This fix must be applied to all
three affected locations where percentage patterns are parsed with the
"(?i)(\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|remaining|left)" regex pattern.

Comment on lines +60 to +62
func setOpenAIUsagePageURL(_ url: String) {
settings.openAI.usagePageURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
}

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

Sanitize OpenAI usage URL before persisting.

Line 61 stores only a trimmed string, which bypasses the sanitization invariant used in OpenAISettings.mergedWithDefaults. This allows invalid URLs to be persisted until a later reload path normalizes them.

Suggested fix
 func setOpenAIUsagePageURL(_ url: String) {
-    settings.openAI.usagePageURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
+    let trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines)
+    settings.openAI.usagePageURL = OpenAIURLValidator.sanitizedUsageURL(trimmed)
 }
🤖 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/Storage/SettingsStore.swift` around lines 60 - 62, The
setOpenAIUsagePageURL method only trims whitespace from the URL before
persisting it, which bypasses the sanitization invariant enforced in
OpenAISettings.mergedWithDefaults. Apply the same sanitization logic used in
OpenAISettings.mergedWithDefaults to the URL string in the setOpenAIUsagePageURL
method to ensure invalid URLs are not persisted and maintain consistency across
all paths that handle this URL.

Comment on lines +259 to +266
private func statusItemClicked(_ sender: AnyObject?) {
if shouldShowContextMenu(for: NSApp.currentEvent) {
showContextMenu()
return
}

togglePopover(sender)
}

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

Close the popover before showing the context menu

When the status item is right-clicked while the popover is already open, the popover stays visible under the context menu. Close it first to keep menu-bar interaction states consistent.

💡 Suggested fix
     private func statusItemClicked(_ sender: AnyObject?) {
         if shouldShowContextMenu(for: NSApp.currentEvent) {
+            if popover.isShown {
+                closePopover(sender)
+            }
             showContextMenu()
             return
         }

         togglePopover(sender)
     }

Also applies to: 294-299

🤖 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 259 - 266, In the
statusItemClicked method, before calling showContextMenu(), you need to close
the popover to ensure it is not visible when the context menu appears. Add a
call to close or hide the popover (likely using a method like hidePopover or
similar) immediately after the shouldShowContextMenu check and before the
showContextMenu() call to maintain consistent menu-bar interaction states.

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

11-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the menu bar wording.

This note currently implies the setting replaces the progress bar, but the feature description says the Cursor percentages are shown alongside it. Please rephrase so the release notes match the shipped behavior.

Suggested wording
-- General setting to hide the menu bar progress bar when Cursor Auto and API percentages are shown instead.
+- General setting to show Cursor Auto and API percentages alongside the existing menu bar progress bar.
🤖 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 `@CHANGELOG.md` around lines 11 - 17, The release note text in the changelog is
misleading because it says the setting hides the menu bar progress bar, while
the shipped behavior keeps the progress bar and Cursor Auto/API percentages
visible together. Rephrase the affected changelog entry to match the actual
behavior described by the nearby “Changed” bullets, keeping the wording
consistent with the menu bar display feature and its percentages-only fallback.
🧹 Nitpick comments (1)
Tests/AIMeterTests/CursorURLValidatorTests.swift (1)

21-32: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin that api2.cursor.sh stays response-only.

These assertions prove the new response host is accepted, but they do not verify that validatedUsageURL(from:) still rejects the same URL. Adding that negative case would lock in the separation between user-configured dashboard URLs and scraped response URLs.

🤖 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/CursorURLValidatorTests.swift` around lines 21 - 32, The
Cursor URL validation test currently only checks that
`isAllowedCursorResponseURLString` accepts `api2.cursor.sh`, but it does not
protect against that host being treated as a user-configurable usage URL. Update
`CursorURLValidatorTests.testAcceptsCursorAPIResponseHost` to also assert that
`validatedUsageURL(from:)` rejects the same `https://api2.cursor.sh/...`
response URL, keeping the response-only behavior separate from dashboard URL
validation.
🤖 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/Cursor/CursorDashboardParser.swift`:
- Around line 244-283: Keep the Cursor plan label matchers in sync with
formattedPlanLabel(_:): the DOM regexes and fallback handling still only
recognize Free/Hobby/Pro+/Ultra, so Team and Enterprise labels can be missed or
misclassified. Update the matching logic in CursorDashboardParser
resolvedPlanLabel(from:) and formattedPlanLabel(_:) so Team/Enterprise are
accepted consistently everywhere, and add a fixture covering Team/Enterprise
billing-page labels plus the legacy "Included in Team" Cursor Plan fallback
case.

---

Outside diff comments:
In `@CHANGELOG.md`:
- Around line 11-17: The release note text in the changelog is misleading
because it says the setting hides the menu bar progress bar, while the shipped
behavior keeps the progress bar and Cursor Auto/API percentages visible
together. Rephrase the affected changelog entry to match the actual behavior
described by the nearby “Changed” bullets, keeping the wording consistent with
the menu bar display feature and its percentages-only fallback.

---

Nitpick comments:
In `@Tests/AIMeterTests/CursorURLValidatorTests.swift`:
- Around line 21-32: The Cursor URL validation test currently only checks that
`isAllowedCursorResponseURLString` accepts `api2.cursor.sh`, but it does not
protect against that host being treated as a user-configurable usage URL. Update
`CursorURLValidatorTests.testAcceptsCursorAPIResponseHost` to also assert that
`validatedUsageURL(from:)` rejects the same `https://api2.cursor.sh/...`
response URL, keeping the response-only behavior separate from dashboard URL
validation.
🪄 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: 7d298ddb-05a3-4ded-8003-c78ee113b5d6

📥 Commits

Reviewing files that changed from the base of the PR and between 2223f58 and 604711e.

📒 Files selected for processing (16)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • SECURITY.md
  • SUPPORT.md
  • Sources/AIMeter/Core/Models.swift
  • Sources/AIMeter/Cursor/CursorDashboardParser.swift
  • Sources/AIMeter/Cursor/CursorURLValidator.swift
  • Sources/AIMeter/Cursor/CursorWebViewScraper.swift
  • Sources/AIMeter/Storage/SettingsStore.swift
  • Sources/AIMeter/UI/MenuPopoverView.swift
  • Tests/AIMeterTests/CursorDashboardParserTests.swift
  • Tests/AIMeterTests/CursorURLValidatorTests.swift
  • Tests/AIMeterTests/SettingsStoreTests.swift
  • docs/release-checklist.md
✅ Files skipped from review due to trivial changes (5)
  • SECURITY.md
  • .github/ISSUE_TEMPLATE/bug_report.md
  • CONTRIBUTING.md
  • docs/release-checklist.md
  • SUPPORT.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • Tests/AIMeterTests/SettingsStoreTests.swift
  • Sources/AIMeter/Cursor/CursorWebViewScraper.swift
  • README.md
  • Sources/AIMeter/Storage/SettingsStore.swift
  • Sources/AIMeter/UI/MenuPopoverView.swift

Comment on lines +244 to +283
private static func resolvedPlanLabel(from object: [String: Any]) -> String? {
if let planInfo = object["planInfo"] as? [String: Any],
let planName = planInfo["planName"] as? String
{
return formattedPlanLabel(planName)
}

if let plan = object["plan"] as? [String: Any],
let label = plan["label"] as? String,
isPlanLabel(label)
{
return label
}

if let planName = object["planName"] as? String {
return formattedPlanLabel(planName)
}

return DashboardParserSupport.jsonLeafStrings(from: object).first(where: isPlanLabel)
}

private static func formattedPlanLabel(_ planName: String) -> String {
let trimmed = DashboardParserSupport.normalizeWhitespace(planName)
guard !trimmed.isEmpty else {
return "Cursor Plan"
}

if isPlanLabel(trimmed) {
return trimmed
}

if trimmed.range(
of: "^(?:Free|Hobby|Pro\\+?|Ultra|Team|Enterprise)\\b",
options: [.regularExpression, .caseInsensitive]
) != nil {
return "Included in \(trimmed)"
}

return trimmed
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the accepted Cursor plan labels.

formattedPlanLabel(_:) now treats Team and Enterprise as valid plans, but the DOM regexes here still only match Included in Free/Hobby/Pro+/Ultra. That makes Team/Enterprise billing pages fall through to .noMatch, and the same pattern drift also leaves legacy "Included in Team" labels on the "Cursor Plan" fallback path. Please keep these matchers in sync and add a Team/Enterprise fixture.

Also applies to: 358-365

🤖 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/Cursor/CursorDashboardParser.swift` around lines 244 - 283,
Keep the Cursor plan label matchers in sync with formattedPlanLabel(_:): the DOM
regexes and fallback handling still only recognize Free/Hobby/Pro+/Ultra, so
Team and Enterprise labels can be missed or misclassified. Update the matching
logic in CursorDashboardParser resolvedPlanLabel(from:) and
formattedPlanLabel(_:) so Team/Enterprise are accepted consistently everywhere,
and add a fixture covering Team/Enterprise billing-page labels plus the legacy
"Included in Team" Cursor Plan fallback case.

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