Display Cursor Auto & API percentages on menu bar#2
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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. ChangesOpenAI Tracking and Multi-Provider Menu Bar
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
Tests/AIMeterTests/DisplayFormattingTests.swift (1)
5-18: ⚡ Quick winAdd boundary-format tests for clamp and threshold behavior.
compactPercentnow 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
📒 Files selected for processing (11)
.cursor/plans/menu_bar_appearance_toggles_080147a0.plan.mdAIMeter.xcodeproj/project.pbxprojREADME.mdSources/AIMeter/App/AppEnvironment.swiftSources/AIMeter/Core/DisplayFormatting.swiftSources/AIMeter/Core/Models.swiftSources/AIMeter/Storage/SettingsStore.swiftSources/AIMeter/UI/MenuBarController.swiftSources/AIMeter/UI/SettingsView.swiftTests/AIMeterTests/DisplayFormattingTests.swiftTests/AIMeterTests/SettingsStoreTests.swift
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
README.md (1)
14-16: 💤 Low valueRefactor 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 winAdd a mixed-availability title test to prevent separator regressions.
Please add a case where both toggles are enabled,
showProgressBaristrue, one provider is disconnected, and the other is connected; assert no leading/trailing pipe intitleText.🤖 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
📒 Files selected for processing (29)
AIMeter.xcodeproj/project.pbxprojREADME.mdSources/AIMeter/App/AIMeterApp.swiftSources/AIMeter/App/AppDelegate.swiftSources/AIMeter/App/AppEnvironment.swiftSources/AIMeter/Core/CursorUsageCoordinator.swiftSources/AIMeter/Core/DashboardStore.swiftSources/AIMeter/Core/DisplayFormatting.swiftSources/AIMeter/Core/MenuBarDisplay.swiftSources/AIMeter/Core/Models.swiftSources/AIMeter/Cursor/CursorDashboardParser.swiftSources/AIMeter/Cursor/CursorWebViewScraper.swiftSources/AIMeter/OpenAI/OpenAIDashboardClient.swiftSources/AIMeter/OpenAI/OpenAIDashboardParser.swiftSources/AIMeter/OpenAI/OpenAISessionManager.swiftSources/AIMeter/OpenAI/OpenAIURLValidator.swiftSources/AIMeter/OpenAI/OpenAIWebViewScraper.swiftSources/AIMeter/Storage/SettingsStore.swiftSources/AIMeter/UI/MenuBarController.swiftSources/AIMeter/UI/MenuPopoverView.swiftSources/AIMeter/UI/SettingsView.swiftSources/AIMeter/UI/SettingsWindowController.swiftSources/AIMeter/UI/UsageMetricCardsRow.swiftTests/AIMeterTests/CursorDashboardParserTests.swiftTests/AIMeterTests/DisplayFormattingTests.swiftTests/AIMeterTests/MenuBarDisplayTests.swiftTests/AIMeterTests/OpenAIDashboardParserTests.swiftTests/AIMeterTests/OpenAIURLValidatorTests.swiftTests/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
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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)) | ||
| } |
There was a problem hiding this comment.
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.
| func setOpenAIUsagePageURL(_ url: String) { | ||
| settings.openAI.usagePageURL = url.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| } |
There was a problem hiding this comment.
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.
| private func statusItemClicked(_ sender: AnyObject?) { | ||
| if shouldShowContextMenu(for: NSApp.currentEvent) { | ||
| showContextMenu() | ||
| return | ||
| } | ||
|
|
||
| togglePopover(sender) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winClarify 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 winPin that
api2.cursor.shstays 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
📒 Files selected for processing (16)
.github/ISSUE_TEMPLATE/bug_report.mdCHANGELOG.mdCONTRIBUTING.mdREADME.mdSECURITY.mdSUPPORT.mdSources/AIMeter/Core/Models.swiftSources/AIMeter/Cursor/CursorDashboardParser.swiftSources/AIMeter/Cursor/CursorURLValidator.swiftSources/AIMeter/Cursor/CursorWebViewScraper.swiftSources/AIMeter/Storage/SettingsStore.swiftSources/AIMeter/UI/MenuPopoverView.swiftTests/AIMeterTests/CursorDashboardParserTests.swiftTests/AIMeterTests/CursorURLValidatorTests.swiftTests/AIMeterTests/SettingsStoreTests.swiftdocs/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
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
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
MenuBarAppearanceSettingsonAppSettings(persisted viaSettingsStore) withshowCursorAutoAPIPercentages(default off);showProgressBarremains in the model and is always merged totruefor now.button.titleso text adapts in light/dark menu bars.DisplayFormatting.compactPercentandcursorAutoAPISuffix(auto:api:)for consistent formatting.README.md.Verification
xcodegen generatexcodebuild -project AIMeter.xcodeproj -scheme AIMeter -destination "platform=macOS" -derivedDataPath ./.derived buildxcodebuild -project AIMeter.xcodeproj -scheme AIMeter -destination "platform=macOS" -derivedDataPath ./.derived testNotes
authExpired/syncFailedif a previous sync is cached (same data as the popover).Summary by CodeRabbit
Release Notes