Add AI provider usage monitoring for Claude and Codex subscriptions - #2827
tranquillum wants to merge 5 commits into
Conversation
|
@tranquillum is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
To use Codex here, create a Codex account and connect to github. |
|
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:
📝 WalkthroughWalkthroughAdds an AI provider usage-monitoring subsystem: provider contracts/registry, Keychain-backed per-account store and editor UI, HTTP/ISO8601 utilities, Claude/Codex providers and fetchers, a visibility-aware polling controller, sidebar footer/popover UI, Settings integration, color/threshold settings, tests, and documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Editor as ProviderAccountEditorSheet
participant Store as ProviderAccountStore
participant Keychain
participant Controller as ProviderAccountsController
User->>Editor: Save account (displayName + credentials)
Editor->>Store: add(providerId, displayName, secret) (async)
Store->>Keychain: SecItemAdd (JSON-encoded secret)
Keychain-->>Store: OSStatus / success
Store->>Store: Persist index to UserDefaults
Store-->>Editor: Return success / throw
Editor->>Controller: refreshNow()
Controller-->>Editor: Publish refresh state / finish
sequenceDiagram
participant Timer
participant Controller as ProviderAccountsController
participant Store as ProviderAccountStore
participant Fetcher as Provider.fetchUsage
participant HTTP as ProviderHTTP
participant UI as ProviderAccountsFooterPanel
Timer->>Controller: Tick()
Controller->>Store: Read accounts
Store-->>Controller: [ProviderAccount]
loop for each account (concurrent)
Controller->>Store: secret(for: account.id)
Store-->>Controller: ProviderSecret
Controller->>Fetcher: fetchUsage(secret) with timeout
Fetcher->>HTTP: GET provider usage endpoint (headers sanitized)
HTTP-->>Fetcher: JSON response / error
Fetcher-->>Controller: ProviderUsageWindows / fetch error
Controller->>Controller: Update snapshots & fetchErrors
end
alt every Nth tick or forced
Controller->>Fetcher: fetchStatus(host, componentFilter)
Fetcher->>HTTP: GET /api/v2/incidents.json
HTTP-->>Fetcher: JSON response
Fetcher-->>Controller: [ProviderIncident] / error
Controller->>UI: Publish incidents & status flags
end
Controller->>UI: Publish snapshots & fetchErrors
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate 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 |
Greptile SummaryThis PR adds an optional AI Usage Monitoring sidebar panel that tracks Claude and Codex subscription usage (5-hour and weekly windows) with Keychain-backed credentials, a 60-second background polling loop, and Statuspage.io incident surfacing. The implementation is thorough: credentials are never logged or written to disk, the Remaining findings are all P2: a possible Confidence Score: 5/5Safe to merge; all remaining findings are P2 maintenance/robustness concerns with no impact on current correctness. Prior P0 compile errors are resolved. The security design (Keychain isolation, header sanitisation, no credential logging, ephemeral URLSession) is sound. The concurrency model (actor isolation, generation-gated tasks, rollback on write failure) is well-considered. Remaining P2 observations are about future-proofing and a low-probability edge case in withTimeout. Sources/Providers/ProviderAccountsController.swift — withTimeout task-group cleanup and fallback NSLog; Sources/Providers/CodexProvider.swift — component filter string stability. Important Files Changed
Sequence DiagramsequenceDiagram
participant AD as AppDelegate
participant PAC as ProviderAccountsController
participant PAS as ProviderAccountStore
participant PR as ProviderRegistry
participant CF as ClaudeUsageFetcher
participant CxF as CodexUsageFetcher
participant SF as StatuspageIOFetcher
participant KC as Keychain
AD->>PAS: shared (warm up index)
AD->>PAC: start()
Note over PAC: DispatchTimer fires every 60s
PAC->>PAS: accounts (snapshot)
loop Per account (parallel)
PAC->>PAS: secret(for: id)
PAS->>KC: SecItemCopyMatching
KC-->>PAS: ProviderSecret
PAS-->>PAC: ProviderSecret
PAC->>PR: provider(id:)
PR-->>PAC: UsageProvider
PAC->>CF: fetchUsage(secret) [Claude]
CF-->>PAC: ProviderUsageWindows
PAC->>CxF: fetchUsage(secret) [Codex]
CxF-->>PAC: ProviderUsageWindows
end
Note over PAC: Every 5th tick
loop Per provider (parallel)
PAC->>SF: fetch(host:componentFilter:)
SF-->>PAC: [ProviderIncident]
end
PAC-->>PAC: publish snapshots / incidents
Note over PAC: SwiftUI views observe @Published state
Reviews (25): Last reviewed commit: "Add unit tests for provider validators, ..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 19
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmuxTests/ProviderTests.swift`:
- Around line 330-346: The test uses the private helper loadSecret(for:) which
is not part of ProviderAccountStore’s public API; replace calls to
loadSecret(for: account.id) with the public method secret(for:) on
ProviderAccountStore (e.g., use store.secret(for: account.id)) and update
assertions to compare the returned ProviderSecret.fields or unwrap the optional
appropriately, and do the same for the other occurrences (including after update
and after remove) so the tests compile against the public API.
- Around line 15-23: The tests testValidJWTAccessTokenAccepted and
testAccessTokenWithWhitespaceTrimmed use literal JWT-like strings that trigger
the repo secret scanner; instead, construct the token at runtime from
non-sensitive fragments (e.g. build header, payload, signature as separate safe
substrings and concatenate them) or replace with an obviously fake placeholder
that still satisfies CodexValidators.isValidAccessToken (for example ensure the
header begins with "eyJ" by using "eyJ" + "header_tail" and combine with
".payload.signature"); update all similar fixtures in this file (including tests
around lines noted) to use the runtime-assembly pattern so no JWT-shaped
literals are checked into the repo.
In `@docs/providers.md`:
- Around line 13-23: Add a language tag to the fenced code block that starts
with "Sources/Providers/" to satisfy MD040; change the opening ``` to ```text
(or another appropriate language) so the block is explicitly marked as plain
text, ensuring the code fence in the docs/providers.md file is updated
accordingly.
In `@docs/usage-monitoring-setup.md`:
- Around line 198-199: The docs state default thresholds as 60 and 85 but the
actual defaults in ProviderUsageColorSettings.swift are initialized to 85 and
95; update the documentation to match the code (or vice versa if code should be
changed). Locate the default threshold properties (e.g., the Low→Mid and
Mid→High default initializers or constants in the ProviderUsageColorSettings
class/struct around the initializer on lines where LowThreshold and MidThreshold
are set) and make them consistent: either change the doc text to `85` and `95`
or change the initializers in ProviderUsageColorSettings (LowThreshold /
MidThreshold) back to `60` and `85`, ensuring both the README/docs and the code
use the same default numbers.
- Around line 217-219: The documentation's Security note claiming cmux calls
/api/v2/incidents/unresolved.json is incorrect because the implementation in
StatuspageIOFetcher (Sources/Providers/StatuspageIOFetcher.swift, function/class
StatuspageIOFetcher) actually requests /api/v2/incidents.json and filters
unresolved incidents client-side; update the docs to reflect this behavior by
replacing the outdated endpoint with /api/v2/incidents.json and note that
unresolved incidents are filtered client-side by StatuspageIOFetcher, or
alternatively change the implementation to call
/api/v2/incidents/unresolved.json if you prefer server-side filtering—ensure the
doc string references StatuspageIOFetcher and the chosen behavior consistently.
In `@Sources/AppDelegate.swift`:
- Around line 2647-2648: The startup is registering provider monitoring
unconditionally; guard it so tests don't start background work: only call
ProviderAccountStore.shared and ProviderAccountsController.shared.start() when
not running under tests by wrapping both of those calls with a check like if
!isRunningUnderXCTest (or an explicit test opt-in env var) so
ProviderAccountsController.start() is skipped during default XCTest launches.
In `@Sources/cmuxApp.swift`:
- Around line 6180-6191: The catch path after calling
ProviderAccountStore.remove(id:) fails to call
ProviderAccountsController.shared.refreshNow(), so when remove(id:) mutates the
in-memory accounts then throws the UI state isn't refreshed; update the Button
action closure (the do-catch around ProviderAccountStore.shared.remove(id:)) to
ensure ProviderAccountsController.shared.refreshNow() is called regardless of
success — either move refreshNow() out of the do-catch to run after it or
explicitly call refreshNow() inside the catch before setting
providerAccountToRemove = nil so snapshots/incidents for the removed account are
refreshed.
In `@Sources/Providers/ClaudeProvider.swift`:
- Line 17: The current validation closures for sessionKey and orgId accept
whitespace-only strings; update the validators to trim whitespace/newlines and
reject empty results (use trimmingCharacters(in:
.whitespacesAndNewlines).isEmpty) and also assign/store the trimmed string
wherever sessionKey and orgId are captured or persisted (so downstream callers
receive trimmed values). Apply this change to the validate closures around the
current occurrence containing validate: { !$0.isEmpty } and the other occurrence
referenced at lines 48–52 (same sessionKey/orgId handling).
- Line 14: Replace the bare placeholder string literals in ClaudeProvider.swift
with localized strings using String(localized:_, defaultValue:_): find the two
occurrences of placeholder: "sk-ant-sid01-…" (the TextField/initializer
placeholders in ClaudeProvider) and change each to something like
String(localized: "claude.placeholder.apiKey", defaultValue: "sk-ant-sid01-…");
register matching localization keys ("claude.placeholder.apiKey", etc.) in your
Localizable.strings (or strings file) so the UI uses localized values instead of
hard-coded literals.
In `@Sources/Providers/ClaudeUsageFetcher.swift`:
- Around line 60-75: Clamp the parsed utilization values to the 0...100 range
before constructing ProviderUsageWindow so out-of-range values cannot flow into
the UI; specifically, after computing fiveHourUtil and sevenDayUtil in
ClaudeUsageFetcher (the variables named fiveHourUtil and sevenDayUtil), replace
their raw int values with clamped versions (e.g. let fiveHourUtilClamped =
max(0, min(100, fiveHourUtil)) and similarly for sevenDayUtil) and use those
clamped values when creating ProviderUsageWindow and returning
ProviderUsageWindows.
In `@Sources/Providers/CodexProvider.swift`:
- Line 35: Replace the bare placeholder string literals used in the account
editor (the placeholder: "eyJhbGciOi…" occurrences) with localized strings; for
each placeholder parameter in the CodexProvider view/editor use
NSLocalizedString (or your app's localization helper) with distinct keys like
"CodexProvider.apiKeyPlaceholder" and "CodexProvider.otherPlaceholder" and
appropriate comment text, update Localizable.strings entries for those keys, and
ensure both placeholder occurrences (the two placeholder parameters) are
switched to use those localization keys.
In `@Sources/Providers/CodexUsageFetcher.swift`:
- Around line 71-83: In parseWindow(_:), stop coercing malformed timing fields
to 0: check dict for the presence of "limit_window_seconds" and
"reset_after_seconds" and if present require Self.doubleValue(...) to return a
value, otherwise throw CodexUsageFetchError.decoding; if a key is absent treat
it as unknown (use nil or default behavior) and only use the parsed doubles to
compute windowSeconds and resetsAt (and still compute resetsAt as
Date(timeIntervalSinceNow: resetsInSeconds) when a valid resetsInSeconds
exists). Update parseWindow, keeping references to ProviderUsageWindow,
Self.doubleValue, and CodexUsageFetchError.decoding.
In `@Sources/Providers/ProviderHTTP.swift`:
- Around line 52-56: The catch block wrapping the await session.data(for:
request) currently converts all errors into ProviderHTTPError.network; update it
to preserve task cancellation by checking if the caught error is a
CancellationError and rethrowing it directly, otherwise wrap the error in
ProviderHTTPError.network. Locate the do/catch around session.data(for: request)
in ProviderHTTP and replace the single catch-throw with a conditional that tests
for CancellationError (or uses Task.isCancelled) before throwing
ProviderHTTPError.network.
- Around line 26-29: The shared URLSession created using
URLSessionConfiguration.ephemeral currently leaves cookie/cache handling
enabled, which can leak in-memory cookies between requests; update the
configuration before returning the session by disabling cookie processing and
caching: set httpShouldSetCookies = false, httpCookieStorage = nil (or
httpCookieAcceptPolicy = .never), set urlCache = nil and requestCachePolicy =
.reloadIgnoringLocalCacheData on the URLSessionConfiguration (while keeping
timeoutIntervalForRequest and timeoutIntervalForResource as-is) so the returned
URLSession(configuration: config) has no automatic cookie or cache handling.
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 347-375: The hard-coded countdown and tooltip strings in metaText
(and the related tooltipText, formatResetTooltip, formatResetVerbose,
relativeCountdown helpers) must be replaced with localized messages instead of
composing English fragments; update these functions to call String(localized:
"...") with appropriate keys (e.g. "reset.countdown.hours",
"reset.countdown.minutes", "reset.countdown.days",
"reset.countdown.lessThanOneMinute", "reset.countdown.inPrefix", etc.) and move
any pluralization into ICU-style .one/.other variants in Localizable.xcstrings
so hours/minutes/days and the "<1m" and "in …" patterns are expressed via
localized templates; ensure you pass numeric values into the localized format
(or use a plural-aware formatter) rather than building "h/m/d" pieces manually
so translations and plural forms render correctly in all locales.
In `@Sources/Sidebar/ProviderAccountsPopover.swift`:
- Around line 299-308: incident.impact and incident.status are raw API tokens
and must be converted to localized, user-facing labels before rendering; create
mapping helpers (e.g., impactLabel(for:) and statusLabel(for:)) or an Incident
extension that maps the raw values to LocalizedStringKey/NSLocalizedString keys,
update the HStack to call those helpers (replace Text(incident.impact) and
Text(incident.status) with Text(impactLabel(for: incident.impact)) and
Text(statusLabel(for: incident.status))), and ensure all mapped strings are
added to localization files.
- Around line 264-267: The popover currently uses the same fallback text for any
missing reset timestamp which causes the Week row to incorrectly show “Session
not started”; update ProviderAccountsPopover so the Session row only shows
"Session not started" when the session-specific timestamp (e.g., sessionReset or
sessionResetDate) is missing, and the Week row uses a different fallback such as
"Weekly reset unknown" (or nothing) when the week-specific timestamp (e.g.,
weeklyReset or weekResetDate) is missing; implement this by branching on the
specific timestamp fields in the view that renders the two rows and using
separate localized strings for each fallback.
- Around line 221-223: Replace the concatenation of separately localized prefix
and dynamic content with a single localized, interpolated string: instead of
Text("\(String(localized: "providers.accounts.popover.fetchedAt", defaultValue:
"Updated")) \(formattedTime(snapshot.fetchedAt))") use a single
String(localized: ...) that includes the formattedTime(snapshot.fetchedAt)
interpolation in the defaultValue (and do the same for the "Resets" line
referenced at lines 259-260). Update the localization keys (e.g.
"providers.accounts.popover.fetchedAt" and the resets key) to contain the full
sentence with a placeholder for the interpolated value so word order is correct
for all locales.
In `@Sources/Sidebar/ProviderAccountsSettingsSection.swift`:
- Around line 74-79: Create a single localized string for the whole summary
(e.g., key providers.accounts.footer.summary) instead of concatenating
fragments; add a new localization with a sensible default like "Sess %d · Week
%d" (or full English "Sess %d · Week %d") and replace the current Text that
builds the string by concatenation with one formatted/localized call that
injects snapshot.session.utilization and snapshot.week.utilization (use
String(format: String(localized: "providers.accounts.footer.summary",
defaultValue: "..."), snapshot.session.utilization, snapshot.week.utilization)
or the SwiftUI-localized-interpolation equivalent) in
ProviderAccountsSettingsSection where the Text currently uses
snapshot.session.utilization and snapshot.week.utilization.
🪄 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: 8e8a623e-fe8e-46c9-9fc9-d389f4a088d1
⛔ Files ignored due to path filters (10)
docs/assets/usage-monitoring-add-profile.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-overview.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-empty.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-filled.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar-collapsed.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar.pngis excluded by!**/*.png
📒 Files selected for processing (26)
GhosttyTabs.xcodeproj/project.pbxprojREADME.mdResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/Providers/ClaudeProvider.swiftSources/Providers/ClaudeUsageFetcher.swiftSources/Providers/CodexProvider.swiftSources/Providers/CodexUsageFetcher.swiftSources/Providers/ProviderAccount.swiftSources/Providers/ProviderAccountStore.swiftSources/Providers/ProviderAccountsController.swiftSources/Providers/ProviderHTTP.swiftSources/Providers/ProviderISO8601DateParser.swiftSources/Providers/ProviderRegistry.swiftSources/Providers/ProviderUsageColorSettings.swiftSources/Providers/StatuspageIOFetcher.swiftSources/Providers/UsageProvider.swiftSources/Sidebar/ProviderAccountEditorSheet.swiftSources/Sidebar/ProviderAccountsFooterPanel.swiftSources/Sidebar/ProviderAccountsPopover.swiftSources/Sidebar/ProviderAccountsSettingsSection.swiftSources/cmuxApp.swiftcmuxTests/ProviderTests.swiftdocs/providers.mddocs/usage-monitoring-setup.md
300a72b to
c0de990
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
Sources/Sidebar/ProviderAccountsFooterPanel.swift (1)
347-537:⚠️ Potential issue | 🟠 MajorLocalize countdown/reset copy instead of composing English fragments.
metaText,tooltipText,formatResetTooltip,formatResetVerbose, andrelativeCountdownstill hard-code fragments liked/h/m,in,<1m, and—. This will not translate correctly and bypasses locale-specific plural forms.Suggested direction
- return "\(days)d" + return String( + localized: "providers.accounts.countdown.days.short", + defaultValue: "\(days)d" + ) - return "\(absolute) (\(relative))" + return String( + localized: "providers.accounts.reset.verbose", + defaultValue: "\(absolute) (\(relative))" + )Also introduce plural-aware keys (e.g.,
.one/.other) inResources/Localizable.xcstringsfor day/hour/minute units and reset phrasing.As per coding guidelines, “All user-facing strings must be localized… Never use bare string literals in SwiftUI Text(), Button(), alert titles, or other UI components.”
Based on learnings, pluralized strings should use ICU-style.oneand.otherkeys.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift` around lines 347 - 537, The code currently composes English fragments directly (e.g., "d/h/m", "in", "<1m", "—") inside metaText, tooltipText, formatResetTooltip, formatResetVerbose, and relativeCountdown; replace these hard-coded fragments with localized, plural-aware strings from Localizable.xcstrings (ICU/.one/.other style) and use Swift's localization APIs (String(localized:...) or NSLocalizedString with plural variants) to format days/hours/minutes and the "in" / "resets" / "not started" connectors; add new keys such as providers.accounts.countdown.days, .hours, .minutes, providers.accounts.countdown.lessThanOneMinute, providers.accounts.usage.placeholder (for "—"), and providers.accounts.usage.resets.{one,other} and update metaText, tooltipText, formatResetTooltip, formatResetVerbose, and relativeCountdown to fetch and format those localized keys rather than concatenating English fragments.Sources/Sidebar/ProviderAccountsSettingsSection.swift (1)
74-74:⚠️ Potential issue | 🟠 MajorUse a key-based localized summary string (not inline localized text).
Line 74 should use a stable localization key with
defaultValueso translators can manage the full sentence reliably.🌐 Proposed fix
- Text(String(localized: "Session \(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%")) + Text( + String( + localized: "providers.accounts.settings.summary", + defaultValue: "Session \(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%" + ) + )Also add
providers.accounts.settings.summarytoResources/Localizable.xcstringswith English and Japanese translations.As per coding guidelines, "All user-facing strings must be localized. Use
String(localized: "key.name", defaultValue: "English text")for every string shown in the UI..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Sidebar/ProviderAccountsSettingsSection.swift` at line 74, Replace the inline localized Text call in ProviderAccountsSettingsSection (the Text(String(localized: "Session \(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%"))) with a key-based localized string using String(localized: "providers.accounts.settings.summary", defaultValue: "Session %d% · Week %d%") (or appropriate placeholder syntax for integers) and pass snapshot.session.utilization and snapshot.week.utilization as the format arguments; then add the key providers.accounts.settings.summary to Resources/Localizable.xcstrings with English and Japanese translations for the full sentence so translators can manage it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Providers/ClaudeUsageFetcher.swift`:
- Around line 35-43: Check for an empty orgId before attempting percent-encoding
and URL creation: if secret.fields["orgId"] (the orgId variable used in
ClaudeUsageFetcher) is empty or only whitespace, throw
ClaudeUsageFetchError.invalidOrgId immediately; then proceed to create
encodedOrgId and build the URL as currently done (the guard that produces
encodedOrgId and url should assume orgId is non-empty). Ensure the check uses
the same orgId variable and preserves the existing error type
(ClaudeUsageFetchError.invalidOrgId) so requests like /organizations//usage
cannot be constructed.
- Around line 52-58: The catch-all currently maps any non-HTTP/network
ProviderHTTPError (e.g., ProviderHTTPError.badResponse) to
ClaudeUsageFetchError.decoding; change the catch sequence in
Sources/Providers/ClaudeUsageFetcher.swift to explicitly handle
ProviderHTTPError.badResponse (map it to a new or existing ClaudeUsageFetchError
case such as .badResponse or .transport) before the generic catch, and narrow
the final catch to only map decoding failures (e.g., catch let err as
DecodingError { throw ClaudeUsageFetchError.decoding(err) }) while converting
any remaining unknown errors to a distinct ClaudeUsageFetchError (e.g., .unknown
or .other) so transport/protocol failures are not misclassified; update any
ClaudeUsageFetchError enum accordingly if needed.
In `@Sources/Providers/CodexUsageFetcher.swift`:
- Around line 54-65: The catch-all currently maps all thrown errors from
ProviderHTTP.getJSONObject to CodexUsageFetchError.decoding, which incorrectly
converts CancellationError into decoding errors; update the error handling in
the do/catch around ProviderHTTP.getJSONObject to rethrow CancellationError
immediately (check for Swift's CancellationError or Task.isCancelled) before
mapping other ProviderHTTPError cases to CodexUsageFetchError (.http, .network,
.decoding). Apply the same change in the analogous StatuspageIOFetcher catch
blocks so CancellationError is propagated unchanged instead of being wrapped.
In `@Sources/Providers/StatuspageIOFetcher.swift`:
- Around line 26-34: The catch-all in the StatuspageIOFetcher async fetch block
is converting CancellationError into StatuspageIOFetchError.decoding; update the
error handling around ProviderHTTP.getJSONObject(url:session:) to preserve task
cancellation by explicitly catching CancellationError and rethrowing it (or
using `throw error`) before the generic `catch { ... }` so only non-cancellation
errors map to StatuspageIOFetchError.decoding; reference
ProviderHTTP.getJSONObject, CancellationError, and
StatuspageIOFetchError.decoding when making the change.
---
Duplicate comments:
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 347-537: The code currently composes English fragments directly
(e.g., "d/h/m", "in", "<1m", "—") inside metaText, tooltipText,
formatResetTooltip, formatResetVerbose, and relativeCountdown; replace these
hard-coded fragments with localized, plural-aware strings from
Localizable.xcstrings (ICU/.one/.other style) and use Swift's localization APIs
(String(localized:...) or NSLocalizedString with plural variants) to format
days/hours/minutes and the "in" / "resets" / "not started" connectors; add new
keys such as providers.accounts.countdown.days, .hours, .minutes,
providers.accounts.countdown.lessThanOneMinute,
providers.accounts.usage.placeholder (for "—"), and
providers.accounts.usage.resets.{one,other} and update metaText, tooltipText,
formatResetTooltip, formatResetVerbose, and relativeCountdown to fetch and
format those localized keys rather than concatenating English fragments.
In `@Sources/Sidebar/ProviderAccountsSettingsSection.swift`:
- Line 74: Replace the inline localized Text call in
ProviderAccountsSettingsSection (the Text(String(localized: "Session
\(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%"))) with a
key-based localized string using String(localized:
"providers.accounts.settings.summary", defaultValue: "Session %d% · Week %d%")
(or appropriate placeholder syntax for integers) and pass
snapshot.session.utilization and snapshot.week.utilization as the format
arguments; then add the key providers.accounts.settings.summary to
Resources/Localizable.xcstrings with English and Japanese translations for the
full sentence so translators can manage it.
🪄 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: 557151b6-f4be-4ffc-a06a-73205d0d2f69
⛔ Files ignored due to path filters (10)
docs/assets/usage-monitoring-add-profile.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-overview.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-empty.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-filled.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar-collapsed.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar.pngis excluded by!**/*.png
📒 Files selected for processing (26)
GhosttyTabs.xcodeproj/project.pbxprojREADME.mdResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/Providers/ClaudeProvider.swiftSources/Providers/ClaudeUsageFetcher.swiftSources/Providers/CodexProvider.swiftSources/Providers/CodexUsageFetcher.swiftSources/Providers/ProviderAccount.swiftSources/Providers/ProviderAccountStore.swiftSources/Providers/ProviderAccountsController.swiftSources/Providers/ProviderHTTP.swiftSources/Providers/ProviderISO8601DateParser.swiftSources/Providers/ProviderRegistry.swiftSources/Providers/ProviderUsageColorSettings.swiftSources/Providers/StatuspageIOFetcher.swiftSources/Providers/UsageProvider.swiftSources/Sidebar/ProviderAccountEditorSheet.swiftSources/Sidebar/ProviderAccountsFooterPanel.swiftSources/Sidebar/ProviderAccountsPopover.swiftSources/Sidebar/ProviderAccountsSettingsSection.swiftSources/cmuxApp.swiftcmuxTests/ProviderTests.swiftdocs/providers.mddocs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (8)
- README.md
- Sources/Providers/ProviderRegistry.swift
- Sources/Providers/ProviderISO8601DateParser.swift
- docs/providers.md
- docs/usage-monitoring-setup.md
- cmuxTests/ProviderTests.swift
- Sources/Providers/UsageProvider.swift
- Sources/Providers/ProviderAccountsController.swift
🚧 Files skipped from review as they are similar to previous changes (8)
- Sources/Providers/ClaudeProvider.swift
- Sources/Providers/CodexProvider.swift
- Sources/Providers/ProviderHTTP.swift
- GhosttyTabs.xcodeproj/project.pbxproj
- Sources/Providers/ProviderAccount.swift
- Sources/Sidebar/ProviderAccountEditorSheet.swift
- Sources/Sidebar/ProviderAccountsPopover.swift
- Sources/Providers/ProviderUsageColorSettings.swift
c0de990 to
e8789f4
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
Sources/Providers/ClaudeProvider.swift (1)
17-17:⚠️ Potential issue | 🟠 MajorNormalize and tighten credential whitespace handling.
Validation currently allows leading/trailing whitespace in otherwise non-empty values (for both
sessionKeyandorgId). If raw values are persisted/sent, this can pass validation but fail provider auth. Please trim at save/use boundaries and reject whitespace-containingorgIdvalues.🔧 Suggested adjustment in this file
CredentialField( id: "sessionKey", label: String(localized: "claude.accounts.editor.sessionKey", defaultValue: "Session key"), placeholder: String(localized: "claude.accounts.editor.sessionKey.placeholder", defaultValue: "sk-ant-sid01-…"), isSecret: true, helpText: String(localized: "claude.accounts.editor.sessionKey.help", defaultValue: "From claude.ai cookies"), - validate: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + validate: { + let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines) + return !trimmed.isEmpty && trimmed.rangeOfCharacter(from: .whitespacesAndNewlines) == nil + } ), @@ static func isValidOrgId(_ orgId: String) -> Bool { let trimmed = orgId.trimmingCharacters(in: .whitespacesAndNewlines) return !trimmed.isEmpty && !trimmed.contains("..") + && trimmed.rangeOfCharacter(from: .whitespacesAndNewlines) == nil && trimmed.rangeOfCharacter(from: segmentReserved) == nil } }Also applies to: 48-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ClaudeProvider.swift` at line 17, The current validate closure allows values with leading/trailing whitespace; update the validation for sessionKey and orgId (the validate: { ... } closures) to trim input when validating and to reject orgId values that contain any internal whitespace (e.g., ensure trimmed != "" and orgId contains no whitespace characters). Additionally, ensure credentials are trimmed at save/use boundaries by trimming sessionKey and orgId where they are persisted or sent (e.g., in the code paths that store or use these values) so raw values never include surrounding whitespace.Sources/Sidebar/ProviderAccountsFooterPanel.swift (1)
347-374:⚠️ Potential issue | 🟠 MajorLocalize/reset-countdown copy is still assembled from English fragments.
metaText,tooltipText,formatResetTooltip(_:),formatResetVerbose(_:), andrelativeCountdown(hours:)still construct user-facing text with hard-coded tokens/order (d/h/m,in,<1m, em dash). This breaks localization and plural handling in non-English locales.As per coding guidelines, "All user-facing strings must be localized... Never use bare string literals in SwiftUI
Text(),Button(), alert titles, or other UI components," and based on learnings, pluralized strings should use ICU-style.one/.otherkeys.Also applies to: 429-437, 470-537
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift` around lines 347 - 374, metaText, tooltipText, formatResetTooltip(_:), formatResetVerbose(_:), and relativeCountdown(hours:) build user-facing strings by concatenating English fragments (e.g., "d/h/m", "in", "<1m", em dash), which prevents localization and proper pluralization; update these functions to use localized format keys (Localizable.strings and Localizable.stringsdict with ICU plural rules) instead of assembling tokens at runtime: create localized templates for day/hour/minute variants (e.g., "{count, plural, one {# day} other {# days}}", "{hours, plural {...}}", and combined templates for "in X" and "<1m" fallback), replace hard-coded symbols like em dash with a localized empty-state string, and call NSLocalizedString/Locale-aware formatting from metaText, tooltipText, formatResetTooltip(_:), formatResetVerbose(_:), and relativeCountdown(hours:) to produce fully localized, plural-aware labels.
🧹 Nitpick comments (6)
Sources/Providers/ProviderAccountStore.swift (4)
126-132: Silent data loss on index decode failure.If the persisted JSON is corrupted,
loadIndex()silently returns an empty array. Users would lose all configured accounts with no indication of what happened.Consider logging in DEBUG to aid troubleshooting:
🛡️ Suggested: Log decode failures in DEBUG
private func loadIndex() -> [ProviderAccount] { - guard let data = userDefaults.data(forKey: indexKey), - let decoded = try? JSONDecoder().decode([ProviderAccount].self, from: data) else { + guard let data = userDefaults.data(forKey: indexKey) else { return [] } - return decoded + do { + return try JSONDecoder().decode([ProviderAccount].self, from: data) + } catch { + `#if` DEBUG + dlog("ProviderAccountStore.loadIndex: decode failed – \(error.localizedDescription)") + `#endif` + return [] + } }As per coding guidelines:
dlogcalls must be wrapped in#if DEBUG/#endif.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderAccountStore.swift` around lines 126 - 132, loadIndex currently swallows JSON decode failures and returns an empty array; update loadIndex to catch and log decoding errors (include the decoding error and the faulty data/context) instead of silently returning [] so failures are visible during debugging, using the userDefaults.data(forKey: indexKey) and JSONDecoder().decode([ProviderAccount].self, from:) code paths; emit the diagnostic via dlog and ensure the dlog call is wrapped in `#if` DEBUG / `#endif` as per guidelines.
177-186: SecItemDelete result ignored before retry.On
errSecDuplicateItem, the delete result (line 179) is discarded. If deletion fails for a reason other than "not found," the subsequent add will also fail — that error will propagate, but the root cause (failed delete) is lost.Minor improvement for debuggability:
♻️ Optional: Check delete status before retry
if status == errSecDuplicateItem { let deleteQuery = Self.matchQuery(service: service, accountId: accountId) - SecItemDelete(deleteQuery as CFDictionary) + let deleteStatus = SecItemDelete(deleteQuery as CFDictionary) + guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else { + throw ProviderAccountStoreError.keychain(deleteStatus) + } let retryStatus = SecItemAdd(query as CFDictionary, nil)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderAccountStore.swift` around lines 177 - 186, When handling errSecDuplicateItem in the add flow, check the return value of SecItemDelete(Self.matchQuery(service: service, accountId: accountId) as CFDictionary) and react to failures before retrying SecItemAdd; if deleteStatus != errSecSuccess and deleteStatus != errSecItemNotFound then throw ProviderAccountStoreError.keychain(deleteStatus) so the root cause (failed delete) is preserved, otherwise proceed to call SecItemAdd and handle its retryStatus as before.
91-114: Acknowledged edge case: stale index entry could resurrect on next launch.As the comment notes (lines 101–106), if
saveIndexfails after keychain deletion, the next launch would restore an account with no secret. Callers ofsecret(for:)would then receive a keychain error.Consider adding cleanup logic in
reload()to prune accounts whose secrets are missing:♻️ Optional: Prune orphaned accounts on reload
func reload() { - accounts = loadIndex() + let loaded = loadIndex() + // Prune accounts whose keychain secrets are missing (e.g., from a prior + // partial-remove failure or manual keychain cleanup). + let valid = loaded.filter { account in + let service = keychainServiceResolver(account.providerId) + return (try? loadSecret(for: account.id, service: service)) != nil + } + if valid.count != loaded.count { + try? saveIndex(valid) + } + accounts = valid }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderAccountStore.swift` around lines 91 - 114, Add pruning of orphaned accounts in reload() so accounts whose keychain secret cannot be found are removed: after loading the persisted index in reload(), iterate the loaded accounts and for each account call secret(for:) (or use deleteSecret/checkSecret helper) to detect missing secrets, build a filtered list excluding accounts whose secret lookup fails with a not-found/keychain-missing error, set accounts = filteredList and call saveIndex(filteredList) if any were removed, and ensure errors other than "not found" are propagated or logged; reference remove(id:), deleteSecret(for:), saveIndex(_:) and secret(for:) to locate related logic.
15-20: Public mutable resolver could be unexpectedly modified.
keychainServiceResolveris avarthat any caller can replace. While this is useful for testing, production code could accidentally or maliciously swap the resolver, potentially misdirecting secrets to wrong keychain services.Consider making it private with an internal/testing-only setter, or marking it with
@_spi(Testing):♻️ Suggested: Restrict mutability to tests
- var keychainServiceResolver: (String) -> String = { providerId in + private(set) var keychainServiceResolver: (String) -> String = { providerId in ProviderRegistry.provider(id: providerId)?.keychainService ?? "com.cmuxterm.app.\(providerId)-accounts" } + + `#if` DEBUG + /// Test-only hook to override keychain service resolution. + func setKeychainServiceResolverForTesting(_ resolver: `@escaping` (String) -> String) { + keychainServiceResolver = resolver + } + `#endif`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderAccountStore.swift` around lines 15 - 20, The public mutable keychainServiceResolver allows external code to replace the resolver; change it so callers cannot mutate it by making it non-publicly settable (e.g., declare keychainServiceResolver as public private(set) or internal) and add a single test-only setter function (e.g., setKeychainServiceResolver(_:)) marked for testing visibility (use `@_spi`(Testing) or a test-only access level) so tests can override it; update references to ProviderRegistry.provider(...) inside keychainServiceResolver as-is and ensure only the new test setter exposes mutation.Sources/Providers/ProviderUsageColorSettings.swift (1)
173-182: Silent black fallback on color-space conversion failure.If
NSColor(self).usingColorSpace(.sRGB)returnsnil, this silently returns black(0, 0, 0). During interpolation, this could produce unexpected dark tints if a user-configured hex is somehow invalid or uses an incompatible color space.Consider logging in DEBUG or returning a fallback that matches the default colors:
♻️ Optional: Add debug logging for failed conversion
var rgbComponents: (red: Double, green: Double, blue: Double) { guard let nsColor = NSColor(self).usingColorSpace(.sRGB) else { + `#if` DEBUG + dlog("Color.rgbComponents: failed to convert to sRGB, returning black fallback") + `#endif` return (red: 0, green: 0, blue: 0) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderUsageColorSettings.swift` around lines 173 - 182, The rgbComponents getter should not silently return black when NSColor(self).usingColorSpace(.sRGB) is nil; update the nil branch in rgbComponents (the NSColor(self).usingColorSpace(.sRGB) check) to (1) emit a debug log (e.g., using os_log or a debug-only logger) describing the failed color-space conversion and the original Color/hex, and (2) return a safer fallback RGB tuple that matches your default UI color (instead of (0,0,0)) — for example use the default color constants used elsewhere in this module or derive components from a known-safe color (the project’s default) so interpolation doesn’t produce an unexpected black tint.Sources/cmuxApp.swift (1)
6516-6523: Consider moving the shared Settings primitives into their own file.Now that these types are reused outside
cmuxApp.swift, keeping them here makes this already-large file harder to navigate and broadens the app entrypoint’s responsibilities. A small dedicatedSettingsLayout-style file would keep the reuse without further growing this file.Also applies to: 6523-6555, 6586-6636, 6700-6739
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/cmuxApp.swift` around lines 6516 - 6523, The Settings layout view primitives (e.g., SettingsSectionHeader, the various Settings* view types, and SettingsConfigurationReview) are now reused outside cmuxApp.swift and should be moved into a dedicated file to reduce the app entrypoint size and improve navigation; create a new file (e.g., SettingsLayout.swift) and relocate the struct/type declarations and any private helper types they need, update their access levels if necessary (make them internal/public rather than file-private), and update imports/usages in other files to reference the moved types so existing references compile without changing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 182-185: The statusText getter currently returns a bare "…"
literal when !isStatusLoaded; change this to use a localized key instead (e.g.
NSLocalizedString("sidebar.status.loading", comment: "Loading status
placeholder") or return a LocalizedStringKey) and update any callers (or the
Text view) to consume the localized value (e.g., Text(LocalizedStringKey) or
Text(NSLocalizedString(...))). Locate the statusText property and replace the
raw "…" with the chosen localized key, and add the corresponding entry to the
Localizable.strings file (key "sidebar.status.loading") so the UI no longer
contains a bare string literal.
---
Duplicate comments:
In `@Sources/Providers/ClaudeProvider.swift`:
- Line 17: The current validate closure allows values with leading/trailing
whitespace; update the validation for sessionKey and orgId (the validate: { ...
} closures) to trim input when validating and to reject orgId values that
contain any internal whitespace (e.g., ensure trimmed != "" and orgId contains
no whitespace characters). Additionally, ensure credentials are trimmed at
save/use boundaries by trimming sessionKey and orgId where they are persisted or
sent (e.g., in the code paths that store or use these values) so raw values
never include surrounding whitespace.
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 347-374: metaText, tooltipText, formatResetTooltip(_:),
formatResetVerbose(_:), and relativeCountdown(hours:) build user-facing strings
by concatenating English fragments (e.g., "d/h/m", "in", "<1m", em dash), which
prevents localization and proper pluralization; update these functions to use
localized format keys (Localizable.strings and Localizable.stringsdict with ICU
plural rules) instead of assembling tokens at runtime: create localized
templates for day/hour/minute variants (e.g., "{count, plural, one {# day} other
{# days}}", "{hours, plural {...}}", and combined templates for "in X" and "<1m"
fallback), replace hard-coded symbols like em dash with a localized empty-state
string, and call NSLocalizedString/Locale-aware formatting from metaText,
tooltipText, formatResetTooltip(_:), formatResetVerbose(_:), and
relativeCountdown(hours:) to produce fully localized, plural-aware labels.
---
Nitpick comments:
In `@Sources/cmuxApp.swift`:
- Around line 6516-6523: The Settings layout view primitives (e.g.,
SettingsSectionHeader, the various Settings* view types, and
SettingsConfigurationReview) are now reused outside cmuxApp.swift and should be
moved into a dedicated file to reduce the app entrypoint size and improve
navigation; create a new file (e.g., SettingsLayout.swift) and relocate the
struct/type declarations and any private helper types they need, update their
access levels if necessary (make them internal/public rather than file-private),
and update imports/usages in other files to reference the moved types so
existing references compile without changing behavior.
In `@Sources/Providers/ProviderAccountStore.swift`:
- Around line 126-132: loadIndex currently swallows JSON decode failures and
returns an empty array; update loadIndex to catch and log decoding errors
(include the decoding error and the faulty data/context) instead of silently
returning [] so failures are visible during debugging, using the
userDefaults.data(forKey: indexKey) and
JSONDecoder().decode([ProviderAccount].self, from:) code paths; emit the
diagnostic via dlog and ensure the dlog call is wrapped in `#if` DEBUG / `#endif` as
per guidelines.
- Around line 177-186: When handling errSecDuplicateItem in the add flow, check
the return value of SecItemDelete(Self.matchQuery(service: service, accountId:
accountId) as CFDictionary) and react to failures before retrying SecItemAdd; if
deleteStatus != errSecSuccess and deleteStatus != errSecItemNotFound then throw
ProviderAccountStoreError.keychain(deleteStatus) so the root cause (failed
delete) is preserved, otherwise proceed to call SecItemAdd and handle its
retryStatus as before.
- Around line 91-114: Add pruning of orphaned accounts in reload() so accounts
whose keychain secret cannot be found are removed: after loading the persisted
index in reload(), iterate the loaded accounts and for each account call
secret(for:) (or use deleteSecret/checkSecret helper) to detect missing secrets,
build a filtered list excluding accounts whose secret lookup fails with a
not-found/keychain-missing error, set accounts = filteredList and call
saveIndex(filteredList) if any were removed, and ensure errors other than "not
found" are propagated or logged; reference remove(id:), deleteSecret(for:),
saveIndex(_:) and secret(for:) to locate related logic.
- Around line 15-20: The public mutable keychainServiceResolver allows external
code to replace the resolver; change it so callers cannot mutate it by making it
non-publicly settable (e.g., declare keychainServiceResolver as public
private(set) or internal) and add a single test-only setter function (e.g.,
setKeychainServiceResolver(_:)) marked for testing visibility (use
`@_spi`(Testing) or a test-only access level) so tests can override it; update
references to ProviderRegistry.provider(...) inside keychainServiceResolver
as-is and ensure only the new test setter exposes mutation.
In `@Sources/Providers/ProviderUsageColorSettings.swift`:
- Around line 173-182: The rgbComponents getter should not silently return black
when NSColor(self).usingColorSpace(.sRGB) is nil; update the nil branch in
rgbComponents (the NSColor(self).usingColorSpace(.sRGB) check) to (1) emit a
debug log (e.g., using os_log or a debug-only logger) describing the failed
color-space conversion and the original Color/hex, and (2) return a safer
fallback RGB tuple that matches your default UI color (instead of (0,0,0)) — for
example use the default color constants used elsewhere in this module or derive
components from a known-safe color (the project’s default) so interpolation
doesn’t produce an unexpected black tint.
🪄 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: 0ddbc727-1951-4250-92b1-c9745c2b9866
⛔ Files ignored due to path filters (10)
docs/assets/usage-monitoring-add-profile.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-overview.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-empty.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-filled.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar-collapsed.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar.pngis excluded by!**/*.png
📒 Files selected for processing (26)
GhosttyTabs.xcodeproj/project.pbxprojREADME.mdResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/Providers/ClaudeProvider.swiftSources/Providers/ClaudeUsageFetcher.swiftSources/Providers/CodexProvider.swiftSources/Providers/CodexUsageFetcher.swiftSources/Providers/ProviderAccount.swiftSources/Providers/ProviderAccountStore.swiftSources/Providers/ProviderAccountsController.swiftSources/Providers/ProviderHTTP.swiftSources/Providers/ProviderISO8601DateParser.swiftSources/Providers/ProviderRegistry.swiftSources/Providers/ProviderUsageColorSettings.swiftSources/Providers/StatuspageIOFetcher.swiftSources/Providers/UsageProvider.swiftSources/Sidebar/ProviderAccountEditorSheet.swiftSources/Sidebar/ProviderAccountsFooterPanel.swiftSources/Sidebar/ProviderAccountsPopover.swiftSources/Sidebar/ProviderAccountsSettingsSection.swiftSources/cmuxApp.swiftcmuxTests/ProviderTests.swiftdocs/providers.mddocs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (9)
- README.md
- Sources/Providers/ProviderISO8601DateParser.swift
- Sources/Providers/ProviderRegistry.swift
- Sources/Providers/ProviderAccount.swift
- docs/usage-monitoring-setup.md
- Sources/Sidebar/ProviderAccountsSettingsSection.swift
- docs/providers.md
- Sources/Sidebar/ProviderAccountsPopover.swift
- cmuxTests/ProviderTests.swift
🚧 Files skipped from review as they are similar to previous changes (8)
- Sources/Providers/CodexProvider.swift
- GhosttyTabs.xcodeproj/project.pbxproj
- Sources/Providers/ProviderHTTP.swift
- Sources/Sidebar/ProviderAccountEditorSheet.swift
- Sources/AppDelegate.swift
- Sources/Providers/StatuspageIOFetcher.swift
- Sources/Providers/CodexUsageFetcher.swift
- Sources/Providers/ProviderAccountsController.swift
e8789f4 to
7aed444
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
Sources/Providers/CodexUsageFetcher.swift (1)
47-49: Consider using a more semantically appropriate error for URL construction failure.The hardcoded URL
"https://chatgpt.com/backend-api/wham/usage"failing to parse is not a credentials issue—it's a programmer error. Throwing.invalidCredentialshere could mislead debugging since the error message suggests the access token is missing.This is a minor concern since the URL is hardcoded and will always parse successfully, making this branch effectively unreachable.
♻️ Potential improvement (optional)
guard let url = URL(string: "https://chatgpt.com/backend-api/wham/usage") else { - throw CodexUsageFetchError.invalidCredentials + throw CodexUsageFetchError.decoding // or a new .internalError case }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/CodexUsageFetcher.swift` around lines 47 - 49, The guard that constructs URL("https://chatgpt.com/backend-api/wham/usage") in CodexUsageFetcher.swift should not throw CodexUsageFetchError.invalidCredentials on a parse failure because that misattributes a programmer/URL bug to credentials; add a more appropriate error case (e.g., CodexUsageFetchError.invalidURL or .internalError) to the CodexUsageFetchError enum and replace the throw in the URL guard with that new case so the error accurately reflects a URL construction failure in the method that builds the usage request.Sources/cmuxApp.swift (1)
6435-6437: Make the reset scope explicit for AI usage monitoring.
resetAllSettings()now resets provider thresholds, but it still preserves saved provider accounts / Keychain credentials. That makes “Reset All Settings” ambiguous for the new feature. Either clear provider accounts here too, or explicitly label/document that credentials are intentionally preserved.Based on learnings,
SettingsView.resetAllSettings()must reset newly added AppStorage toggles to defaults.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/cmuxApp.swift` around lines 6435 - 6437, The reset behavior is ambiguous: update the reset block around WorkspaceTabColorSettings.reset(), ProviderUsageColorSettings.shared.resetToDefaults(), and reloadWorkspaceTabColorSettings() to either also clear stored provider accounts/Keychain entries (call the method that removes saved provider accounts or Keychain credentials) or explicitly document/rename the action to indicate credentials are preserved; additionally update SettingsView.resetAllSettings() to explicitly reset the new AppStorage-backed toggles to their default values so newly added AI usage monitoring switches return to defaults when resetting settings.cmuxTests/ProviderTests.swift (1)
131-141: Force unwrap of optional TimeZone.While
TimeZone(identifier: "UTC")is extremely unlikely to fail, using force unwrap in tests can produce confusing crash reports. Consider using a guard orXCTUnwrap.🛡️ Suggested fix
func testParsedDateHasCorrectComponents() { let date = ProviderISO8601DateParser.parse("2026-04-10T14:30:00Z") XCTAssertNotNil(date) let calendar = Calendar(identifier: .gregorian) - var components = calendar.dateComponents(in: TimeZone(identifier: "UTC")!, from: date!) + let utc = try XCTUnwrap(TimeZone(identifier: "UTC")) + let parsedDate = try XCTUnwrap(date) + let components = calendar.dateComponents(in: utc, from: parsedDate) XCTAssertEqual(components.year, 2026)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxTests/ProviderTests.swift` around lines 131 - 141, The test testParsedDateHasCorrectComponents force-unwraps TimeZone(identifier: "UTC"), which can crash the test; update the test to safely unwrap the timezone (using guard let tz = TimeZone(identifier: "UTC") else { XCTFail("..."); return } or let tz = try XCTUnwrap(TimeZone(identifier: "UTC"))) and then use tz when calling calendar.dateComponents; keep the rest of the assertions unchanged and reference ProviderISO8601DateParser.parse for locating the test.Sources/Providers/ProviderUsageColorSettings.swift (1)
173-182: Consider handling nil color space conversion gracefully.
NSColor(self).usingColorSpace(.sRGB)can return nil for colors that can't be converted (e.g., pattern colors). While unlikely for usage bar colors, the fallback to black(0, 0, 0)might produce unexpected results. Consider logging or using a more visible fallback.💡 Optional: Add debug logging for conversion failures
var rgbComponents: (red: Double, green: Double, blue: Double) { guard let nsColor = NSColor(self).usingColorSpace(.sRGB) else { + `#if` DEBUG + dlog("ProviderUsageColorSettings: Failed to convert color to sRGB") + `#endif` return (red: 0, green: 0, blue: 0) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ProviderUsageColorSettings.swift` around lines 173 - 182, The rgbComponents getter currently returns black when NSColor(self).usingColorSpace(.sRGB) fails; update rgbComponents to handle conversion failures more gracefully by: first attempting alternative conversions (e.g., NSColor(self).usingColorSpace(.deviceRGB) or using self.cgColor?.components), then falling back to a more visible default (e.g., mid-gray or magenta) rather than silent black, and add a debug log or assertion (use your logging facility or assertionFailure) to record when NSColor(self).usingColorSpace(.sRGB) returns nil so conversion issues are visible; keep references to the rgbComponents property and the NSColor(self).usingColorSpace(.sRGB) call to locate and modify the code.Sources/Sidebar/ProviderAccountsPopover.swift (1)
275-284: Consider locale-aware time formatting.The
DateFormatteruses a fixeden_US_POSIXlocale, which is correct for machine-readable timestamps but not ideal for user-facing display. Since this shows in the "Updated HH:mm:ss" popover text, consider using the user's locale for display formatting.🌐 Suggested fix
private static let timeFormatterHMS: DateFormatter = { let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss" - formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeStyle = .medium return formatter }()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Sidebar/ProviderAccountsPopover.swift` around lines 275 - 284, The time formatter uses a fixed en_US_POSIX locale (timeFormatterHMS) which is machine-oriented; update it to be user-locale-aware by replacing the custom dateFormat with a localized time style (e.g., formatter.timeStyle = .medium) and set formatter.locale = Locale.current (or omit setting locale) so formattedTime(_:) uses a user-friendly localized string; update the static timeFormatterHMS initializer and keep formattedTime(_:) calling Self.timeFormatterHMS.string(from:).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Sidebar/ProviderAccountEditorSheet.swift`:
- Around line 131-135: The catch block in ProviderAccountEditorSheet currently
overrides every failure with a generic errorMessage, which hides actionable
ProviderAccountStoreError details; update the catch to inspect the thrown error
(e.g., if let storeError = error as? ProviderAccountStoreError) and set
errorMessage from the storeError’s localizedDescription or a mapped localized
message for specific cases, otherwise fall back to the existing generic
localized "providers.accounts.error.loadSecret" message so Keychain/not-found
guidance is preserved.
---
Nitpick comments:
In `@cmuxTests/ProviderTests.swift`:
- Around line 131-141: The test testParsedDateHasCorrectComponents force-unwraps
TimeZone(identifier: "UTC"), which can crash the test; update the test to safely
unwrap the timezone (using guard let tz = TimeZone(identifier: "UTC") else {
XCTFail("..."); return } or let tz = try XCTUnwrap(TimeZone(identifier: "UTC")))
and then use tz when calling calendar.dateComponents; keep the rest of the
assertions unchanged and reference ProviderISO8601DateParser.parse for locating
the test.
In `@Sources/cmuxApp.swift`:
- Around line 6435-6437: The reset behavior is ambiguous: update the reset block
around WorkspaceTabColorSettings.reset(),
ProviderUsageColorSettings.shared.resetToDefaults(), and
reloadWorkspaceTabColorSettings() to either also clear stored provider
accounts/Keychain entries (call the method that removes saved provider accounts
or Keychain credentials) or explicitly document/rename the action to indicate
credentials are preserved; additionally update SettingsView.resetAllSettings()
to explicitly reset the new AppStorage-backed toggles to their default values so
newly added AI usage monitoring switches return to defaults when resetting
settings.
In `@Sources/Providers/CodexUsageFetcher.swift`:
- Around line 47-49: The guard that constructs
URL("https://chatgpt.com/backend-api/wham/usage") in CodexUsageFetcher.swift
should not throw CodexUsageFetchError.invalidCredentials on a parse failure
because that misattributes a programmer/URL bug to credentials; add a more
appropriate error case (e.g., CodexUsageFetchError.invalidURL or .internalError)
to the CodexUsageFetchError enum and replace the throw in the URL guard with
that new case so the error accurately reflects a URL construction failure in the
method that builds the usage request.
In `@Sources/Providers/ProviderUsageColorSettings.swift`:
- Around line 173-182: The rgbComponents getter currently returns black when
NSColor(self).usingColorSpace(.sRGB) fails; update rgbComponents to handle
conversion failures more gracefully by: first attempting alternative conversions
(e.g., NSColor(self).usingColorSpace(.deviceRGB) or using
self.cgColor?.components), then falling back to a more visible default (e.g.,
mid-gray or magenta) rather than silent black, and add a debug log or assertion
(use your logging facility or assertionFailure) to record when
NSColor(self).usingColorSpace(.sRGB) returns nil so conversion issues are
visible; keep references to the rgbComponents property and the
NSColor(self).usingColorSpace(.sRGB) call to locate and modify the code.
In `@Sources/Sidebar/ProviderAccountsPopover.swift`:
- Around line 275-284: The time formatter uses a fixed en_US_POSIX locale
(timeFormatterHMS) which is machine-oriented; update it to be user-locale-aware
by replacing the custom dateFormat with a localized time style (e.g.,
formatter.timeStyle = .medium) and set formatter.locale = Locale.current (or
omit setting locale) so formattedTime(_:) uses a user-friendly localized string;
update the static timeFormatterHMS initializer and keep formattedTime(_:)
calling Self.timeFormatterHMS.string(from:).
🪄 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: 4ca2f3b2-4b91-4fa3-b28a-9cb838c022b1
⛔ Files ignored due to path filters (10)
docs/assets/usage-monitoring-add-profile.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-overview.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-empty.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-filled.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar-collapsed.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar.pngis excluded by!**/*.png
📒 Files selected for processing (26)
GhosttyTabs.xcodeproj/project.pbxprojREADME.mdResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/Providers/ClaudeProvider.swiftSources/Providers/ClaudeUsageFetcher.swiftSources/Providers/CodexProvider.swiftSources/Providers/CodexUsageFetcher.swiftSources/Providers/ProviderAccount.swiftSources/Providers/ProviderAccountStore.swiftSources/Providers/ProviderAccountsController.swiftSources/Providers/ProviderHTTP.swiftSources/Providers/ProviderISO8601DateParser.swiftSources/Providers/ProviderRegistry.swiftSources/Providers/ProviderUsageColorSettings.swiftSources/Providers/StatuspageIOFetcher.swiftSources/Providers/UsageProvider.swiftSources/Sidebar/ProviderAccountEditorSheet.swiftSources/Sidebar/ProviderAccountsFooterPanel.swiftSources/Sidebar/ProviderAccountsPopover.swiftSources/Sidebar/ProviderAccountsSettingsSection.swiftSources/cmuxApp.swiftcmuxTests/ProviderTests.swiftdocs/providers.mddocs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (8)
- README.md
- Sources/Providers/ProviderISO8601DateParser.swift
- Sources/Providers/ProviderHTTP.swift
- Sources/Providers/ProviderRegistry.swift
- Sources/Providers/ProviderAccount.swift
- docs/usage-monitoring-setup.md
- docs/providers.md
- Sources/Providers/UsageProvider.swift
🚧 Files skipped from review as they are similar to previous changes (6)
- Sources/Providers/CodexProvider.swift
- Sources/Providers/ClaudeProvider.swift
- Sources/Providers/ClaudeUsageFetcher.swift
- GhosttyTabs.xcodeproj/project.pbxproj
- Sources/Sidebar/ProviderAccountsSettingsSection.swift
- Sources/Sidebar/ProviderAccountsFooterPanel.swift
2fb8e80 to
bd98722
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
Sources/Providers/ClaudeProvider.swift (2)
11-18:⚠️ Potential issue | 🟠 MajorNormalize
sessionKey, not just validate it.This validator trims only to decide validity; it does not normalize the stored value. A pasted cookie with a trailing newline still passes here, then goes out verbatim in
ClaudeUsageFetcherand turns into a hard-to-diagnose 401. Please trim before persistence, or trim again before building theCookieheader.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ClaudeProvider.swift` around lines 11 - 18, The sessionKey validator only trims for validation but doesn't normalize the stored value, so trailing whitespace/newlines can cause 401s; update the code that persists or uses the session key (e.g., the CredentialField with id "sessionKey" and/or the code in ClaudeUsageFetcher that builds the Cookie header) to trim() the value (removing leading/trailing whitespace/newlines) before saving or before constructing the Cookie header so the sent cookie is normalized.
48-52:⚠️ Potential issue | 🟡 MinorReject embedded whitespace in
orgId.
trimmingCharacters(in:)only removes edge whitespace, so values likeorg idoruuid\nsuffixstill pass validation and get percent-encoded into a different path. This should fail in the editor instead of becoming a remote 404.Proposed fix
static func isValidOrgId(_ orgId: String) -> Bool { let trimmed = orgId.trimmingCharacters(in: .whitespacesAndNewlines) return !trimmed.isEmpty && !trimmed.contains("..") + && trimmed.rangeOfCharacter(from: .whitespacesAndNewlines) == nil && trimmed.rangeOfCharacter(from: segmentReserved) == nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Providers/ClaudeProvider.swift` around lines 48 - 52, The current validation trims edge whitespace but still allows embedded whitespace (e.g., "org id"), so update isValidOrgId(_ orgId: String) to reject any internal whitespace: after creating trimmed, add a check that trimmed.rangeOfCharacter(from: .whitespacesAndNewlines) == nil (or equivalent) in the boolean chain along with the existing segmentReserved check and non-empty check; reference the function isValidOrgId, the trimmed variable and segmentReserved to locate where to add this additional validation.Sources/Sidebar/ProviderAccountsFooterPanel.swift (1)
347-374:⚠️ Potential issue | 🟠 MajorThe reset/countdown copy is still assembled from English fragments.
metaText,tooltipText,formatResetTooltip,formatResetVerbose, andrelativeCountdownstill emit rawd/h/m,in …, and<1mtext. That keeps both the footer and popover reset strings out ofLocalizable.xcstrings, and plural/order rules can’t be translated cleanly.As per coding guidelines, "All user-facing strings must be localized. Use
String(localized: "key.name", defaultValue: "English text")for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages)," and based on learnings, pluralized strings in this repo should use ICU-style.one/.otherkeys.Also applies to: 428-537
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift` around lines 347 - 374, The countdown strings (metaText, tooltipText, formatResetTooltip, formatResetVerbose, relativeCountdown) are currently built from hard-coded English fragments like "d/h/m", "in …", and "<1m"; replace these with localized ICU-style strings using String(localized: "key", defaultValue: ...) and plural forms (.one/.other) so all user-facing text is pulled from Localizable.xcstrings; create keys for days/hours/minutes (e.g. "reset.days", "reset.hours", "reset.minutes") and for whole phrases ("reset.in", "reset.less-than-minute") and use those localized templates to format the output in each function (use pluralization for numeric units and avoid concatenating raw "d/h/m" fragments), ensuring metaText returns a localized compact form and tooltipText/formatResetTooltip/formatResetVerbose/relativeCountdown return fully localized sentences.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Providers/CodexUsageFetcher.swift`:
- Around line 92-95: Clamp the computed usedPercent to the 0...100 range before
converting/rounding and building ProviderUsageWindow: compute a clamped value
(e.g., clamp = min(max(usedPercent, 0), 100)) and then use Int(clamp.rounded())
for the utilization passed into ProviderUsageWindow; update the return in the
function that constructs ProviderUsageWindow in CodexUsageFetcher.swift to use
that clamped value instead of the raw usedPercent.
In `@Sources/Providers/ProviderAccountsController.swift`:
- Around line 87-99: scheduleTick/currentTask currently blocks future ticks when
a provider fetch hangs because tick(generation:force:) awaits network calls with
no timeout; wrap those provider fetch awaits in a controller-level timeout so a
slow/hung request fails fast (treated like any other fetch failure) and allows
the task to complete and currentTask/isRefreshing to be cleared. Concretely, add
a small helper (e.g., withTimeout or use a racing Task that throws after N
seconds) and apply it around the provider network calls invoked from
tick(generation:force:) (the same sites referenced in the review where provider
fetches occur), ensure any timeout throws are handled the same as other fetch
errors, and that currentTask = nil and isRefreshing are reset when the
timed-out/failed task finishes.
In `@Sources/Providers/StatuspageIOFetcher.swift`:
- Around line 52-55: The current check uses componentFilter and then always
computes componentNames and returns nil when disjoint, which filters out
page-wide incidents with an empty "components" array; update the logic in the
block that references componentFilter, components, componentNames and dict so
the intersection check runs only when the payload actually includes component
attachments (i.e., only when components is non-empty) — e.g., if
components.isEmpty, skip the disjoint guard and allow the incident through;
otherwise compute componentNames and apply the existing guard
!componentNames.isDisjoint(with: componentFilter).
In `@Sources/Sidebar/ProviderAccountsPopover.swift`:
- Around line 223-225: Replace the hard-coded English timestamp label and POSIX
formatter usage in ProviderAccountsPopover.swift (the Text(...) that uses
formattedTime(snapshot.fetchedAt) and the similar block at 275-284) with a
locale-aware time formatter and an explicit localized string key; specifically,
format snapshot.fetchedAt with a locale-aware Date.FormatStyle or
DateFormatter.localizedString(for: , dateStyle: .none, timeStyle: .short) (so it
respects user locale/12h vs 24h), and embed that formatted time into
String(localized: "provider.updated_at", defaultValue: "Updated {0}") (or
equivalent localized key) instead of the English literal so the UI text is fully
localized. Ensure you update both occurrences (the one at ~223 and the block at
275-284) to use the same localized key and locale-aware formatting function.
---
Duplicate comments:
In `@Sources/Providers/ClaudeProvider.swift`:
- Around line 11-18: The sessionKey validator only trims for validation but
doesn't normalize the stored value, so trailing whitespace/newlines can cause
401s; update the code that persists or uses the session key (e.g., the
CredentialField with id "sessionKey" and/or the code in ClaudeUsageFetcher that
builds the Cookie header) to trim() the value (removing leading/trailing
whitespace/newlines) before saving or before constructing the Cookie header so
the sent cookie is normalized.
- Around line 48-52: The current validation trims edge whitespace but still
allows embedded whitespace (e.g., "org id"), so update isValidOrgId(_ orgId:
String) to reject any internal whitespace: after creating trimmed, add a check
that trimmed.rangeOfCharacter(from: .whitespacesAndNewlines) == nil (or
equivalent) in the boolean chain along with the existing segmentReserved check
and non-empty check; reference the function isValidOrgId, the trimmed variable
and segmentReserved to locate where to add this additional validation.
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 347-374: The countdown strings (metaText, tooltipText,
formatResetTooltip, formatResetVerbose, relativeCountdown) are currently built
from hard-coded English fragments like "d/h/m", "in …", and "<1m"; replace these
with localized ICU-style strings using String(localized: "key", defaultValue:
...) and plural forms (.one/.other) so all user-facing text is pulled from
Localizable.xcstrings; create keys for days/hours/minutes (e.g. "reset.days",
"reset.hours", "reset.minutes") and for whole phrases ("reset.in",
"reset.less-than-minute") and use those localized templates to format the output
in each function (use pluralization for numeric units and avoid concatenating
raw "d/h/m" fragments), ensuring metaText returns a localized compact form and
tooltipText/formatResetTooltip/formatResetVerbose/relativeCountdown return fully
localized sentences.
🪄 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: c44dfd99-80d7-4f03-9529-842c780517e4
⛔ Files ignored due to path filters (10)
docs/assets/usage-monitoring-add-profile.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-editor-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-overview.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-claude.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-popover-codex.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-empty.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-settings-filled.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar-collapsed.pngis excluded by!**/*.pngdocs/assets/usage-monitoring-sidebar.pngis excluded by!**/*.png
📒 Files selected for processing (26)
GhosttyTabs.xcodeproj/project.pbxprojREADME.mdResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/Providers/ClaudeProvider.swiftSources/Providers/ClaudeUsageFetcher.swiftSources/Providers/CodexProvider.swiftSources/Providers/CodexUsageFetcher.swiftSources/Providers/ProviderAccount.swiftSources/Providers/ProviderAccountStore.swiftSources/Providers/ProviderAccountsController.swiftSources/Providers/ProviderHTTP.swiftSources/Providers/ProviderISO8601DateParser.swiftSources/Providers/ProviderRegistry.swiftSources/Providers/ProviderUsageColorSettings.swiftSources/Providers/StatuspageIOFetcher.swiftSources/Providers/UsageProvider.swiftSources/Sidebar/ProviderAccountEditorSheet.swiftSources/Sidebar/ProviderAccountsFooterPanel.swiftSources/Sidebar/ProviderAccountsPopover.swiftSources/Sidebar/ProviderAccountsSettingsSection.swiftSources/cmuxApp.swiftcmuxTests/ProviderTests.swiftdocs/providers.mddocs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (7)
- README.md
- Sources/Providers/ProviderISO8601DateParser.swift
- Sources/Providers/ProviderRegistry.swift
- docs/providers.md
- GhosttyTabs.xcodeproj/project.pbxproj
- docs/usage-monitoring-setup.md
- Sources/Providers/UsageProvider.swift
🚧 Files skipped from review as they are similar to previous changes (4)
- Sources/Providers/ProviderAccount.swift
- Sources/Providers/ProviderHTTP.swift
- Sources/Sidebar/ProviderAccountsSettingsSection.swift
- Sources/Providers/ProviderUsageColorSettings.swift
a203aa1 to
30eabb8
Compare
|
@codex review |
@tranquillum I have started the AI code review. It will take a few minutes to complete. |
|
To use Codex here, create a Codex account and connect to github. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 52014 🏁 Script executed: Length of output: 65424
🔴 High1. Localized string keys containing runtime values (
|
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | 🔴 High | ProviderAccountsPopover.swift |
Runtime-interpolated String(localized:) keys defeat localization |
| 2 | 🔴 High | ProviderAccountEditorSheet.swift, ProviderAccountStore.swift |
Synchronous Keychain on main thread |
| 3 | 🟡 Medium | ClaudeUsageFetcher.swift |
Cookie value not sanitized for semicolons |
| 4 | 🟡 Medium | ProviderAccountStore.swift |
Stale index entry on remove() failure resurrects secret-less account |
| 5 | 🟡 Medium | ProviderAccountsController.swift |
Timer tick silently dropped during long in-flight fetch |
| 6 | 🔵 Low | ProviderTests.swift |
ProviderUsageColorSettings tests pollute UserDefaults.standard |
| 7 | 🔵 Low | ProviderAccountsFooterPanel.swift |
emptyPlaceholder unreachable in normal flow |
| 8 | 🔵 Low | ProviderTests.swift |
Missing tests for ProviderSecret redaction and header sanitization |
| 9 | 🔵 Nit | CodexUsageFetcher.swift |
CFBoolean check via === works but CFGetTypeID is more idiomatic |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30eabb82ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
5 issues found across 37 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="README.md">
<violation number="1" location="README.md:88">
P2: Markdown links were added inside an HTML table cell; use HTML anchors to ensure links render/click reliably in the README.</violation>
</file>
<file name="Sources/Providers/ProviderAccountStore.swift">
<violation number="1" location="Sources/Providers/ProviderAccountStore.swift:128">
P2: Index decode errors are silently treated as empty state, causing account disappearance and potential orphaned keychain credentials without diagnostics.</violation>
</file>
<file name="cmuxTests/ProviderTests.swift">
<violation number="1" location="cmuxTests/ProviderTests.swift:174">
P2: Clamp tests are ineffective because they only assert non-nil on a non-optional return type instead of asserting equality with boundary colors.</violation>
</file>
<file name="Sources/Providers/CodexUsageFetcher.swift">
<violation number="1" location="Sources/Providers/CodexUsageFetcher.swift:87">
P2: `reset_after_seconds` rejects JSON `null` (`NSNull`) and throws decoding, causing avoidable fetch failure for an optional field.</violation>
</file>
<file name="Sources/Providers/ClaudeUsageFetcher.swift">
<violation number="1" location="Sources/Providers/ClaudeUsageFetcher.swift:80">
P2: Weekly quota parsing silently defaults missing/invalid `seven_day` utilization to 0%, which can mask response/schema problems and display misleading usage.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
3c7187d to
e959d35
Compare
|
@codex review |
@tranquillum I have started the AI code review. It will take a few minutes to complete. |
|
✅ Actions performedReview triggered.
|
ac98434 to
a0f83e4
Compare
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
Adds the persistence layer for AI usage credentials: - AIUsageKeychain: async wrappers around SecItem* with kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrSynchronizable=false, errSecDuplicateItem fallback to SecItemUpdate, strict missing-item handling on update, and a presence probe used by orphan pruning. All Keychain calls run on Task.detached(priority: .userInitiated) and forward cancellation. - AIUsageAccountStore @mainactor ObservableObject: keychain-first add/update with rollback on index persistence failure, remove performs Keychain delete first, orphan pruning runs only on errSecItemNotFound (locked-Keychain rows survive). UserDefaults index key is c11-prefixed (c11.aiusage.accounts.index). - c11Tests/AIUsageTests.swift introduces the test bag with two subclasses for this commit: AIUsageAccountStoreRoundTripTests exercises add/secret/update/remove and persistence across two store instances; AIUsageSecretRedactionTests verifies description, debugDescription, dump, and string interpolation never leak field values while Codable round-trip still preserves them. Wires the new files into the c11 Sources and c11Tests Sources build phases. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… poller Adds the controller and shared networking pieces: - AIUsageHTTP: ephemeral URLSession factory (no cookie storage, no URLCache, reload-ignoring-local cache policy), GET-JSON helper that surfaces CancellationError on cancellation, and sanitizeHeaderValue that strips control chars plus ; and , so a header value can never smuggle in CRLF or cookie attribute separators. - AIUsageISO8601DateParser.parse(_:) tries fractional seconds first then plain internet-date-time. - AIUsageStatusPagePoller: allowlists status.claude.com and status.openai.com, rejects hosts containing / or :, filters out resolved/postmortem/completed incidents, and applies the optional componentFilter only to incidents that have a non-empty components list (page-wide outages always pass through). - AIUsagePoller @mainactor controller: 60s DispatchSourceTimer on the com.stage11.c11.aiusage.timer queue, 20s per-fetch timeout via withThrowingTaskGroup, occlusion-aware skip, status fetch every 5th tick, tick coalescing with hasPendingTick, taskGeneration so a stale completion can't mutate state for a cancelled tick, and a localizedFetchErrorMessage that only surfaces errorDescription for errors marked AIUsageAppOwnedError so raw OS wording does not leak. Tests-only init takes a tickBody and visibilityProvider seam so lifecycle tests can drive the controller without a real timer. - c11Tests/AIUsageTests.swift gains AIUsageISO8601DateParserTests, AIUsageHTTPSessionTests, AIUsageStatusPagePollerHostTests, and AIUsagePollerLifecycleTests (start idempotency + refreshNow-after-stop). Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the first concrete provider: - ClaudeAIValidators: orgId rejects empty, .., /, :, ?, #, %, whitespace, and control characters; sessionKey rejects empty, ; , and control characters; strippedSessionKey peels off one or more leading `sessionKey=` prefixes after trimming whitespace. - Providers.claude: the AIUsageProvider value with both credential fields, status section title, helpDoc URL, fetchUsage closure, and fetchStatus closure that calls the allowlisted Claude statuspage with a component filter (claude.ai, Claude API, Claude Code). - ClaudeAIUsageFetcher: hits https://claude.ai/api/organizations/<percent-encoded orgId>/usage with a sanitized Cookie header carrying the stripped session key, re-runs both validators on the stored value, parses five_hour.utilization and seven_day.utilization (rejects booleans and fractional numbers, clamps 0...100), and surfaces a ClaudeAIUsageFetchError that conforms to AIUsageAppOwnedError so AIUsagePoller can forward its localized messages. - AIUsageRegistry.all now includes Providers.claude (was [] in commit 1; commit 5 will append Providers.codex). - Tests: ClaudeAIValidatorTests (orgId / sessionKey accept and reject, strippedSessionKey behavior) and AIUsageRegistryClaudeTests (registry round-trip + unknown-id returns nil). Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the second concrete provider:
- CodexAIValidators: accessToken trims surrounding whitespace, must
split on `.` into exactly 3 segments, the first segment starts with
`eyJ` (JWT URL-safe encoding of `{"`), all chars are in the
base64url + `.` + `=` allowed set; accountId is optional, empty is
accepted, but the literal `null` sentinel and any whitespace or
control characters are rejected.
- Providers.codex: AIUsageProvider value with both credential fields,
status section title, helpDoc URL, fetchUsage closure, and
fetchStatus closure that calls the allowlisted OpenAI statuspage
with a Codex-only component filter.
- CodexAIUsageFetcher: hits
https://chatgpt.com/backend-api/wham/usage with sanitized
Authorization and chatgpt-account-id headers (the account-id
header is omitted when accountId is empty), parses
rate_limit.primary_window and rate_limit.secondary_window, requires
limit_window_seconds > 0, rejects negative reset_after_seconds,
and surfaces a CodexAIUsageFetchError that conforms to
AIUsageAppOwnedError.
- AIUsageRegistry.all now returns [Providers.claude, Providers.codex].
- Tests: CodexAIValidatorTests (jwt shape, trim, accountId empty/null
paths) and AIUsageRegistryCodexTests (registry contains codex,
unique provider ids, every UI provider carries credential fields).
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the user-configurable usage-bar color model: - AIUsageColorSettings @mainactor ObservableObject with low/mid/high hex colors, low/mid and mid/high thresholds (1...98 / 2...99, enforced ordered), an interpolation toggle, resetToDefaults, and a color(for percent:) that produces a Color whether interpolation is on (sRGB blend across the configured stops) or off (discrete buckets). UserDefaults keys are c11-prefixed (c11.aiusage.color.low / .mid / .high / .lowMidThreshold / .midHighThreshold / .interpolate). - Color(usageHex:) initializer that accepts an optional leading `#` and a strict 6-hex-digit body, .usageHexString that round-trips back to canonical "#RRGGBB", and .rgbComponents that goes through NSColor's sRGB color space so interpolation is in the same space the picker presents. - AIUsageColorSettingsTests covers default fallthrough, threshold ordering and clamping, reset, color() bounds, and an interpolation- off bucket sanity check; AIUsageColorHexTests covers parsing. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ditor sheet + popover - Drops `private` from `SettingsSectionHeader`, `SettingsCard`, `SettingsCardRow`, and `SettingsCardDivider` in `Sources/c11App.swift` so module-internal AIUsage UI files can compose them. No public surface changes (these helpers were not exposed before). - Sources/AIUsage/UI/AIUsageResetLabel.swift: pure ResetLabel reducer used by the footer; awaitingRefresh / sessionNotStarted / weekResetUnknown / resetsAt(Date). - Sources/AIUsage/UI/AIUsageStatusRanking.swift: pure status helpers; worstImpactSeverity, worstIncident, statusText (localized). - Sources/AIUsage/UI/AIUsageEditorSheet.swift: SwiftUI sheet driven by AIUsageAccountStore.shared; pre-fills existing secret on .task while disabling fields so an async load can't clobber early keystrokes; trims and saves; refreshNow on success; surfaces AIUsageStoreError.errorDescription or LocalizedError messages. - Sources/AIUsage/UI/AIUsagePopover.swift: status section (loading / fetch failed / all-ok / per-incident rows) plus action section (Add, Edit selected, Refresh now); status page link opens via NSWorkspace.open. - Sources/AIUsage/UI/AIUsageSettingsSection.swift: account list with Edit/Remove rows, single-button or Menu Add row depending on UI provider count, orphan provider card with Remove only, color picker rows, threshold steppers, interpolation toggle, and reset button. Rendered above the Agent Skills section in commit 8. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e in AppDelegate - Sources/AIUsage/UI/AIUsageFooterView.swift: per-provider section with collapse-toggle (persisted under c11.aiusage.collapsed.<id>), account rows showing Session and Week bars colored by AIUsageColorSettings, fetch error fallthrough, and a popover trigger for AIUsagePopover. Renders EmptyView when no accounts exist so users who never set up an account see no visual change. - Sources/ContentView.swift: SidebarFooter now wraps both branches of its #if DEBUG / #else in a VStack that places AIUsageFooterView above SidebarDevFooter / SidebarFooterButtons, matching the plan. - Sources/AppDelegate.swift: starts AIUsagePoller and warms AIUsageAccountStore.shared in applicationDidFinishLaunching, gated by isRunningUnderXCTest so test runs do not initiate polling; applicationWillTerminate calls AIUsagePoller.shared.stop(). - Sources/c11App.swift: SettingsNavigationTarget gains .aiUsage (mapped to .agentsAutomation), SettingsView gains the @StateObject references and editor/remove state, agentsAutomationSettingsPage renders AIUsageSettingsSection above the existing Agent Skills section, the SettingsView body root carries the editor sheet, remove confirmationDialog, and remove error alert, and resetAllSettings calls AIUsageColorSettings.shared.resetToDefaults so global Reset restores the bar palette (account credentials are intentionally NOT wiped by reset). Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- docs/ai-usage-monitoring.md: setup guide for the AI Usage Monitoring panel. Covers where it appears, the privacy guarantees (Keychain only, ephemeral URLSession, no credential logging, allowlisted status hosts), per-provider setup steps for Claude (sessionKey + orgId from claude.ai cookies / network requests) and Codex (jq-on-~/.codex/auth.json), multiple accounts per provider, and troubleshooting for 401, 404, status loading, and occlusion-aware polling. - docs/privacy-endpoints.md: project-wide outbound HTTP catalog. Adds the four AI Usage Monitoring endpoints (claude.ai usage, chatgpt.com WHAM, status.claude.com, status.openai.com) and the hard guarantees that backstop them (allowlisted hosts, Keychain only, ephemeral URLSession, sanitized headers, no credential logging, occlusion gate). Future features that contact new hosts must update this file. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…follow Adds 83 English-only entries to Resources/Localizable.xcstrings for the AI Usage Monitoring feature. Coverage: - provider editor labels, placeholders, and help text for both Claude (sessionKey, orgId) and Codex (accessToken, accountId). - store / generic errors (keychain status, decoding, not found, unknown provider, fetch failed, timeout, load secret). - Claude fetcher errors (invalid orgId/sessionKey, http auth, http, bad response, decoding, network). - Codex fetcher errors (invalid accessToken/accountId, http auth, http 404, http, bad response, decoding, network). - editor sheet (add/edit titles, name, cancel, save, help link). - settings section (title, color section title, low/mid/high color rows, threshold row, interpolate toggle, reset button). - account row (add, add menu label, edit, remove, summary, unknown provider message). - remove flow (confirmation title/action/body/cancel, error title/ ok/notFound/decoding/keychain/unknown). - footer (expand/collapse, accessibility label, session/week labels). - status / popover (section, openPage, loading, fetchFailed, allOk, maintenance/minor/major/critical, refreshNow). Per the c11 convention, only the English `defaultValue` is authored here; the translator pass for ja, uk, ko, zh-Hans, zh-Hant, ru is a follow-up. No em-dashes per memory feedback_no_em_dashes. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…onitoring strings Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nitoring strings Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sheet identity B1: Add `import Security` and `import SwiftUI` to c11Tests/AIUsageTests.swift so Security symbols (kSecClass, SecItemDelete, CFDictionary) and SwiftUI Color references resolve. B2: Wire the sidebar popover Add/Edit closures to present AIUsageEditorSheet. AIUsageFooterView now owns an editorRequest @State and a .sheet(item:) modifier. onAdd sets a request with no account; onEdit sets the tapped account. I7: Replace the per-provider sheet binding (aiUsageEditorProvider) with a composite AIUsageEditorRequest so SwiftUI re-creates the sheet view per edited account. Editing two accounts on the same provider no longer shares @State between the two sheets. Updated all call sites (settings page Edit/Add and the new footer popover wiring) to set/clear aiUsageEditorRequest. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…status timeout, I6 secondary_window, I8 pending force I1: CodexAIUsageFetcher.parseWindow now mirrors ClaudeAIUsageFetcher.parseUtilization: malformed used_percent (nil, NSNull, Bool, non-integer) throws .decoding instead of clamping to 0%. Tests cover nil/NSNull/Bool/string -> .decoding and a valid integer happy path. I3: AIUsageAccountStore.pruneOrphanAccountsIfNeeded now snapshots accounts at start, computes remove ids from the snapshot, and re-reads accounts after the async probes complete to compute the final keep list. Concurrent add() during the prune is no longer clobbered. Adds os_log(.info) when prune actually removes something. I4: AIUsageAccountStore.load() catches decode errors with os_log(.error) including the error description; it does NOT delete the underlying UserDefaults blob so a future migration can still recover. Test verifies a malformed blob survives a load() call. I5: AIUsagePoller wraps the status-page fetchStatus() in runWithTimeout(perFetchTimeout) so a stalled status fetch cannot hold the tick indefinitely. Timeout maps to statusFetchFailed[provider.id] = true. I6: CodexAIUsageFetcher.parseWindows treats missing secondary_window as a soft case and returns AIUsageWindow(utilization: 0, resetsAt: nil, windowSeconds: 604800) when the key is absent. The footer reset-label logic for weekResetUnknown already handles this. Test covers a payload with primary_window but no secondary_window. I8: AIUsagePoller now tracks pendingForce separately. When in-flight, scheduleTick(force:) sets pendingForce |= force. After the in-flight task completes, the follow-up scheduleTick uses pendingForce instead of false, preserving the original force intent through one re-entrant queue. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The AIUsageFooterView bar function now renders a small trailing reset countdown when providerUsageResetLabel resolves to .resetsAt(date), using a shared RelativeDateTimeFormatter (.abbreviated). Refresh granularity stays at the 60s tick (no per-second timer). Two new localized strings: aiusage.reset.accessibility and aiusage.reset.resetsIn (English source values only; translations follow in a separate pass per project policy). Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s, remove order, AppStorage, test flake, test assertions) M1: AIUsageEditorSheet.canSave trims each value before the empty-check and before passing to validate, matching what save() already does. M2: AIUsageSettingsSection main ForEach filters out orphan accounts so an unknown-provider row does not render twice (once in the main card, once in the orphan card). M3: AIUsagePopover refresh button uses the injected `poller` instead of AIUsagePoller.shared, restoring the DI seam for previews/tests. M4: docs/ai-usage-monitoring.md adds top-level `## Claude` and `## Codex` headings so the helpDocURL fragments (#claude / #codex) resolve. M5: AIUsageAccountStore.remove() persists the index first, commits the in- memory list, then deletes the Keychain item. A failed Keychain delete leaves the next prune cycle to clean the orphan rather than abandoning the user with a stale row. M6: AIUsageFooterView extracts the per-provider section into a private AIUsageFooterProviderSection subview that uses @AppStorage with a per-id key, removing the in-body UserDefaults.standard.bool read. M7: testRefreshNowAfterStopDoesNotRestartPolling and testStartIsIdempotent replace 50 ms Task.sleep waits with XCTestExpectation fulfilled from inside tickBody. Subsequent assertions yield the runloop instead of sleeping. M8: testStartIsIdempotent now asserts after the first tick has fired (so the second start() is observed against a real baseline). testColorForPercentBoundsAreClamped asserts that values below 0 / above 100 clamp to the 0% / 100% buckets and that 0% and 100% do not produce the same color. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
S11: Replaces an em-dash in AIUsageRegistry.swift's `ui` doc comment with
a comma. docs/ai-usage-monitoring.md and docs/privacy-endpoints.md scanned
clean (no em-dashes present).
S2: Adds a code comment at ClaudeAIUsageFetcher.swift's org-id percent-
encoding site noting that urlPathAllowed includes '/' and '.' but
ClaudeAIValidators.isValidOrgId rejects both, so path traversal would
require Keychain write access.
S6: Adds a comment to AIUsageHTTP.sanitizeHeaderValue noting that ';' and
',' are stripped intentionally for today's known providers (claude.ai
session keys, Codex JWTs); a future token type with legitimate separators
must add a provider-specific sanitizer.
S13: Localization audit run across Sources/ AIUsage call sites and
Resources/Localizable.xcstrings. No gaps found in the load-bearing
direction (every code-referenced "aiusage.*" key has a catalog entry).
A handful of catalog-only stale entries exist (aiusage.error.unknownProvider,
aiusage.help.link, aiusage.remove.error.{decoding,keychain,notFound},
aiusage.status.section); leaving for a future cleanup pass.
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The marker protocol that decides whether an error's errorDescription is safe to surface to the UI is moved from AIUsagePoller's bottom-of-file private declaration to its own Sources/Errors/C11AppOwnedError.swift. Single requirement (var isAppOwned: Bool) is unchanged. The new home makes the seam discoverable for any future user-facing error pipeline, not just AI usage. Conformances updated: - ClaudeAIUsageFetchError, CodexAIUsageFetchError: AIUsageAppOwnedError -> C11AppOwnedError - AIUsageFetchTimeoutError: now conforms to C11AppOwnedError so its localized "Request timed out." string flows through the gate (it was already routed via a special-case branch; the conformance makes the intent explicit). - AIUsageStoreError: newly conforms to C11AppOwnedError. Its localized descriptions were already authored by c11 and surfaced via a special-case in localizedFetchErrorMessage; the conformance lets future call sites use the protocol uniformly. AIUsagePoller.localizedFetchErrorMessage now checks for C11AppOwnedError instead of the AIUsage-specific protocol. Project file updated to include the new Sources/Errors/ file. Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
What changed? Adds an optional AI Usage Monitoring panel in the sidebar footer that tracks subscription usage (Session + Week windows) for your AI coding agents, with support for multiple accounts per provider (e.g. Personal + Work).
Ships two providers out of the box:
claude.ai5-hour and weekly rate limits.Introduces a generic
UsageProvider/ProviderRegistryabstraction so new providers can be added without touching UI code (seedocs/providers.md).Integrates provider status pages (
status.claude.com,status.openai.com) so incidents show up next to the usage bar.All credentials are stored only in macOS Keychain under a per-provider service name — never written to disk in plaintext, never logged, only sent to the provider's own API.
New settings section Settings → AI Usage Monitoring for adding/editing/removing accounts.
Full setup docs at
docs/usage-monitoring-setup.md, linked from README. The guide includes step-by-step instructions on how to obtain every credential needed for each provider (Claude session key + organization ID, Codex access token + account ID)Why? cmux positions itself as a terminal "for managing multiple AI coding agents". Running several agents against Claude and Codex subscriptions makes it very easy to hit the 5-hour or weekly limit mid-task without warning. Surfacing real-time usage and pace directly in the sidebar — where the agents live — lets users plan work against the quota instead of being surprised by a rate-limit error.
Personal note. I've been running this in my own locally-built cmux for about a week across my daily work with Claude and Codex, and it's genuinely changed how I pace agent runs — I stopped getting surprised by 5-hour limits mid-task. I'd be happy if it turns out useful for anyone else too. Open to feedback on scope, API choices, or anything that would make it easier to merge.
Screenshots
Sidebar footer with usage bars:
Per-row popover with reset times and provider status (Claude / Codex):
Settings → AI Usage Monitoring with multiple accounts:
Add-profile sheet (Claude / Codex):
Collapsed sidebar (only status dots visible):
Testing
cmuxTests/ProviderTests.swiftcover the provider registry, credential validators, usage color threshold settings, and ISO8601 parsing.Today HH:mm/Tomorrow HH:mm/MMM d, HH:mmformats.Demo Video
Not applicable — this change has no dynamic behavior to record. The screenshots above and the
docs/usage-monitoring-setup.mdguide cover every state (empty, filled, collapsed, popover, editor sheet).Review Trigger (Copy/Paste as PR comment)
Checklist
Summary by cubic
Adds a sidebar footer panel that tracks Claude and Codex subscription usage per account with Session (5h) and Week bars, reset times, incident badges, and per‑provider popovers. Background polling starts on launch (skipped in tests) and stops on quit; credentials are stored in macOS Keychain.
New Features
SettingsNavigationTargetand settings search.UsageProvider,ProviderRegistry) with ephemeral HTTP sessions, ISO8601 parsing, and allowlisted Statuspage.io hosts; ships Claude and Codex providers with usage + status integration. Background controller (ProviderAccountsController) is wired into app startup/shutdown;ProviderAccountStorepersists accounts and stores secrets in Keychain.Docs & Tests
docs/providers.md,docs/usage-monitoring-setup.md; README updated with screenshots and localized strings.Written for commit 2e4d602. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests