Skip to content

Add AI provider usage monitoring for Claude and Codex subscriptions#2827

Open
tranquillum wants to merge 5 commits intomanaflow-ai:mainfrom
tranquillum:feature/agents-usage-monitoring-sidebar-panel
Open

Add AI provider usage monitoring for Claude and Codex subscriptions#2827
tranquillum wants to merge 5 commits intomanaflow-ai:mainfrom
tranquillum:feature/agents-usage-monitoring-sidebar-panel

Conversation

@tranquillum
Copy link
Copy Markdown

@tranquillum tranquillum commented Apr 11, 2026

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 — tracks claude.ai 5-hour and weekly rate limits.
    • Codex — tracks ChatGPT/Codex 5-hour and weekly rate limits.
  • Introduces a generic UsageProvider / ProviderRegistry abstraction so new providers can be added without touching UI code (see docs/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:

usage-monitoring-overview

Per-row popover with reset times and provider status (Claude / Codex):

usage-monitoring-popover-claude usage-monitoring-popover-codex

Settings → AI Usage Monitoring with multiple accounts:

usage-monitoring-settings-filled

Add-profile sheet (Claude / Codex):

usage-monitoring-editor-claude usage-monitoring-editor-codex

Collapsed sidebar (only status dots visible):

usage-monitoring-sidebar-collapsed

Testing

  • Unit tests in cmuxTests/ProviderTests.swift cover the provider registry, credential validators, usage color threshold settings, and ISO8601 parsing.
  • Manual verification on macOS with live accounts:
    • Added multiple Claude and Codex accounts, confirmed each row renders its own Session/Week bars and refreshes on the 60-second timer.
    • Verified credentials round-trip through macOS Keychain (removed account → re-added → usage bars repopulate; no plaintext on disk).
    • Verified reset-time popover shows correct Today HH:mm / Tomorrow HH:mm / MMM d, HH:mm formats.
    • Verified collapsing a provider section keeps only the status dot visible and the section disappears entirely when all accounts for a provider are removed.
    • Verified status-page fetchers surface active incidents next to the usage row.
    • Verified that removing credentials does not affect unrelated providers.

Demo Video

Not applicable — this change has no dynamic behavior to record. The screenshots above and the docs/usage-monitoring-setup.md guide cover every state (empty, filled, collapsed, popover, editor sheet).

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Adds a sidebar footer panel to track Claude and Codex subscription usage per account, with Session (5h) and Week bars, reset times, incident badges, and per-provider popovers. Credentials are stored in macOS Keychain; background polling starts on launch and stops on quit.

  • New Features

    • Footer panel with per-account Session/Week bars, reset times, incident badges, and popovers; supports multiple accounts per provider and hides when none exist.
    • Settings → Usage Monitoring: add/edit/remove accounts, plus customizable usage bar colors, thresholds, and optional interpolation.
    • Generic UsageProvider + ProviderRegistry with background polling, short timeouts, Statuspage.io integration, and ephemeral HTTP sessions; disabled during tests.
  • Docs & Tests

    • New docs: docs/providers.md, docs/usage-monitoring-setup.md; README section with screenshot and links.
    • Unit tests for validators (Claude session/org, Codex JWT/account), registry/store, HTTP/ISO8601 parsing, color settings, and settings flows.

Written for commit 6a6d9c0. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • AI provider usage sidebar footer with per-account Session (5h) and Week (7d) bars, provider popovers, account editor sheet, Settings integration for account management and usage-color customization, plus built-in Claude and Codex provider integrations with secure Keychain credential storage and automated usage/status polling.
  • Bug Fixes

    • Skip provider startup during tests; stop provider controller on shutdown; improved footer layout padding and polling visibility/cancellation.
  • Documentation

    • New provider docs, setup guide, README entry and screenshot.
  • Tests

    • New provider-focused unit and integration tests.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 11, 2026

@tranquillum is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an AI provider usage-monitoring subsystem: provider contracts and registry; Keychain-backed per-account store; shared HTTP/date utilities; Claude and Codex fetchers; a visibility-aware polling controller; sidebar footer/popover, settings and editor UIs; color/threshold settings; tests and docs.

Changes

Cohort / File(s) Summary
Project & Docs
GhosttyTabs.xcodeproj/project.pbxproj, README.md, docs/providers.md, docs/usage-monitoring-setup.md, docs/assets/usage-monitoring-overview.png
Xcode entries for new sources; README feature row and two new docs + screenshot added.
Core Models & Registry
Sources/Providers/UsageProvider.swift, Sources/Providers/ProviderRegistry.swift, Sources/Providers/ProviderAccount.swift
Adds provider/usage/incident/snapshot models, credential field/provider contract, ProviderAccount/ProviderSecret with redaction, and ProviderRegistry/Providers namespace.
HTTP, Date & Status
Sources/Providers/ProviderHTTP.swift, Sources/Providers/ProviderISO8601DateParser.swift, Sources/Providers/StatuspageIOFetcher.swift
Shared HTTP/session helpers with header sanitization and error mapping, ISO8601 parser, and Statuspage.io incident fetcher with host validation and parsing.
Providers & Fetchers
Sources/Providers/ClaudeProvider.swift, Sources/Providers/ClaudeUsageFetcher.swift, Sources/Providers/CodexProvider.swift, Sources/Providers/CodexUsageFetcher.swift
Registers Claude and Codex providers, adds validation helpers, and implements provider-specific async usage fetchers with typed errors and parsing logic.
Account Store & Keychain
Sources/Providers/ProviderAccountStore.swift
Main-actor ProviderAccountStore singleton: UserDefaults index, Keychain JSON storage, add/update/remove/secret APIs, SecItem wrappers, and rollback semantics.
Polling Controller
Sources/Providers/ProviderAccountsController.swift
Visibility-aware polling controller with generation-based concurrency, periodic usage and less-frequent status polls, timeout helper, and published snapshots/fetchErrors/incidents.
Color Settings
Sources/Providers/ProviderUsageColorSettings.swift
UserDefaults-backed color hex and threshold settings, clamping/invariant logic, interpolation support, hex parsing helpers, preview and reset API.
Sidebar UI
Sources/Sidebar/ProviderAccountsFooterPanel.swift, Sources/Sidebar/ProviderAccountsPopover.swift, Sources/Sidebar/ProviderAccountEditorSheet.swift
Footer panel with collapsible provider sections, aligned usage rows and status label; popover with accounts/status/actions; account add/edit sheet wired to store/controller.
Settings & App Wiring
Sources/Sidebar/ProviderAccountsSettingsSection.swift, Sources/cmuxApp.swift, Sources/AppDelegate.swift, Sources/ContentView.swift
Adds Provider Accounts settings section and navigation target; editor/removal flows and UI state; AppDelegate start/stop wiring gated during XCTest; sidebar footer inserted.
Tests
cmuxTests/ProviderTests.swift
New XCTest suite covering validators, ISO8601 parser, ProviderSecret redaction, color settings, registry, ProviderHTTP, and ProviderAccountStore round-trip tests.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Editor as ProviderAccountEditorSheet
    participant Store as ProviderAccountStore
    participant Keychain
    participant Controller as ProviderAccountsController

    User->>Editor: Save account (name + credentials)
    Editor->>Store: add(providerId, displayName, secret)
    Store->>Keychain: SecItemAdd / SecItemUpdate (JSON payload)
    Keychain-->>Store: Success / OSStatus error
    Store->>Store: Persist accounts index (UserDefaults)
    Store-->>Editor: Return success / error
    Editor->>Controller: refreshNow()
    Controller-->>Editor: Refresh scheduled/completed
Loading
sequenceDiagram
    participant Timer
    participant Controller as ProviderAccountsController
    participant Store as ProviderAccountStore
    participant Fetcher
    participant HTTP as ProviderHTTP
    participant UI as ProviderAccountsFooterPanel

    Timer->>Controller: Tick()
    Controller->>Store: Load accounts
    Store-->>Controller: [ProviderAccount]
    loop per account
        Controller->>Store: secret(for: account.id)
        Store-->>Controller: ProviderSecret
        Controller->>Fetcher: fetchUsage(secret)
        Fetcher->>HTTP: GET /usage (provider-specific) with headers
        HTTP-->>Fetcher: JSON response or error
        Fetcher-->>Controller: ProviderUsageWindows / fetch error
        Controller->>Controller: Update snapshots & fetchErrors
    end
    alt status cadence
        Controller->>Fetcher: fetchStatus(host, componentFilter)
        Fetcher->>HTTP: GET /api/v2/incidents.json
        HTTP-->>Fetcher: JSON response
        Fetcher-->>Controller: [ProviderIncident] / error
    end
    Controller->>UI: Publish snapshots & incidents
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐇 I nibble keys and tidy threads,
I count the bars from sessions to weeks,
Secrets tucked where moonlight peeks,
Colors bloom as usage speaks,
I hop and guard these tiny tweaks.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the main feature: AI provider usage monitoring for Claude and Codex subscriptions.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering what changed, why it matters, testing approach, screenshots, and a complete checklist. It addresses all major sections of the template.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 36 files

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 11, 2026

Greptile Summary

Adds a sidebar footer panel tracking Claude and Codex subscription usage (Session/Week windows) per account, with secure Keychain credential storage, a 60-second background polling loop, Statuspage.io incident integration, and per-provider settings. The implementation is careful about off-main Keychain I/O, actor-reentrancy on account mutations, header sanitization, and URL allowlisting, and all three previously flagged issues (two loadSecret compile errors and the duplicate-save race) are resolved in the current code.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style suggestions that do not affect correctness or security.

No P0/P1 issues found. The keychain rollback, actor-reentrancy handling, header injection defenses, and isRefreshing lifecycle are all correctly implemented. The one flagged issue (duplicate UserDefaults key string in ProviderSection) is a minor maintenance hazard with no present runtime impact. Previous review concerns are resolved.

Sources/Sidebar/ProviderAccountsFooterPanel.swift — minor collapsed-key string duplication worth cleaning up before future refactors.

Important Files Changed

Filename Overview
Sources/Providers/ProviderAccountStore.swift Keychain I/O runs off-main via detached tasks; actor-reentrancy is handled by re-reading accounts after awaits; orphan pruning and rollback semantics are well-documented and correct.
Sources/Providers/ProviderAccountsController.swift Timer/occlusion-driven polling with generation-based cancellation; isRefreshing lifecycle is correctly managed through all exit paths including stop(); parallel task groups with per-fetch timeouts.
Sources/Providers/ProviderHTTP.swift Ephemeral URLSession (no disk cookie/cache), sanitizeHeaderValue strips control chars and cookie separators as a last-line defense; cancellation correctly forwarded from URLError.
Sources/Providers/ClaudeUsageFetcher.swift Re-validates stored credentials before use; percent-encodes org ID against a tight alphanumeric allowlist; boolean/utilization type guard prevents silent schema regression.
Sources/Providers/CodexUsageFetcher.swift Re-validates access token and account ID before building headers; handles string-encoded numbers; rejects NaN/Inf values that would corrupt integer conversion; NSNull/nil distinction for reset_after_seconds.
Sources/Providers/StatuspageIOFetcher.swift Allowlist-based host validation prevents SSRF to arbitrary hosts; correctly surfaces page-wide (empty-components) incidents alongside filtered ones.
Sources/Sidebar/ProviderAccountsFooterPanel.swift Footer panel and usage rows are well-structured; minor: collapsedKey string for UserDefaults is computed in two separate places (init and the collapsedKey property) — if they diverge, persist/load targets different keys silently.
Sources/Sidebar/ProviderAccountEditorSheet.swift isSaving guard prevents duplicate submission; isLoadingCredentials locks credential fields during keychain load; error paths correctly reset isSaving; .task modifier cancels on view disappear.
Sources/Providers/ProviderUsageColorSettings.swift Threshold invariants enforced through setThresholds gatekeeper; color interpolation correctly clamped; test-injectable UserDefaults init; sRGB fallback is documented.
cmuxTests/ProviderTests.swift Tests cover behavioral paths (validators, store round-trip, keychain, color settings) with isolated UserDefaults suites and teardown cleanup; previously-flagged loadSecret compile errors are resolved (uses correct secret(for:) API).
Sources/Providers/ClaudeProvider.swift Validator rejects path-traversal characters (/, .., :, ;, =, ?, #) in orgId; session key prefix stripping loops to handle double-prefix pastes; status page filter covers relevant components.

Sequence Diagram

sequenceDiagram
    participant App as AppDelegate
    participant PAC as ProviderAccountsController
    participant PAS as ProviderAccountStore
    participant KC as Keychain
    participant API as Provider API
    participant SP as Statuspage.io

    App->>PAC: start()
    PAC->>PAC: schedule DispatchSourceTimer (60s)
    loop Every 60s (or on hidden to visible)
        PAC->>PAS: accounts (read, MainActor)
        par For each account (TaskGroup)
            PAC->>KC: secret(for:) via detached task
            KC-->>PAC: ProviderSecret
            PAC->>API: fetchUsage(secret)
            API-->>PAC: ProviderUsageWindows
        end
        PAC->>PAC: update snapshots / fetchErrors (MainActor)
        alt Every 5th tick or force
            par For each providerId (TaskGroup)
                PAC->>SP: fetch incidents
                SP-->>PAC: [ProviderIncident]
            end
            PAC->>PAC: update incidents / statusLoaded (MainActor)
        end
    end
    App->>PAC: stop()
    PAC->>PAC: cancel timer + currentTask
Loading

Reviews (19): Last reviewed commit: "Add unit tests for provider validators, ..." | Re-trigger Greptile

Comment thread cmuxTests/ProviderTests.swift Outdated
Comment on lines +358 to +359
secret: ProviderSecret(fields: ["accessToken": "eyJhbGciOiJ.eyJzdWIi.sig"])
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 loadSecret is not accessible from tests — compile error

The test calls store.loadSecret(for: account.id), but no method with that name and single-UUID signature exists on ProviderAccountStore. The private method is loadSecret(for accountId: UUID, service: String) (two arguments, private), which @testable import cannot expose. The intended public API is secret(for id: UUID). This call — and the identical one in testIndexPersistsAcrossInstances — would produce a compile error that prevents the entire test target from building.

Suggested change
secret: ProviderSecret(fields: ["accessToken": "eyJhbGciOiJ.eyJzdWIi.sig"])
)
let loaded = try store.secret(for: account.id)

XCTAssertEqual(reloaded.accounts.first?.id, createdId)
XCTAssertEqual(try reloaded.loadSecret(for: createdId).fields["accessToken"], "eyJhbGciOiJ.eyJzdWIi.sig")
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Same loadSecret call in second test — compile error

Same issue as in testAddLoadUpdateRemoveRoundTrip: reloaded.loadSecret(for: createdId) does not match any accessible method and would fail to compile. The correct call is reloaded.secret(for: createdId).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d289f66 and 300a72b.

⛔ Files ignored due to path filters (10)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md

Comment thread cmuxTests/ProviderTests.swift
Comment thread cmuxTests/ProviderTests.swift Outdated
Comment thread docs/providers.md Outdated
Comment thread docs/usage-monitoring-setup.md Outdated
Comment thread docs/usage-monitoring-setup.md Outdated
Comment thread Sources/Sidebar/ProviderAccountsFooterPanel.swift
Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsPopover.swift
Comment thread Sources/Sidebar/ProviderAccountsSettingsSection.swift Outdated
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from 300a72b to c0de990 Compare April 13, 2026 20:52
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
Sources/Sidebar/ProviderAccountsFooterPanel.swift (1)

347-537: ⚠️ Potential issue | 🟠 Major

Localize countdown/reset copy instead of composing English fragments.

metaText, tooltipText, formatResetTooltip, formatResetVerbose, and relativeCountdown still hard-code fragments like d/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) in Resources/Localizable.xcstrings for 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 .one and .other keys.

🤖 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 | 🟠 Major

Use a key-based localized summary string (not inline localized text).

Line 74 should use a stable localization key with defaultValue so 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.summary to Resources/Localizable.xcstrings with 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

📥 Commits

Reviewing files that changed from the base of the PR and between 300a72b and c0de990.

⛔ Files ignored due to path filters (10)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/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

Comment thread Sources/Providers/ClaudeUsageFetcher.swift Outdated
Comment thread Sources/Providers/ClaudeUsageFetcher.swift
Comment thread Sources/Providers/CodexUsageFetcher.swift
Comment thread Sources/Providers/StatuspageIOFetcher.swift
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from c0de990 to e8789f4 Compare April 13, 2026 21:21
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
Sources/Providers/ClaudeProvider.swift (1)

17-17: ⚠️ Potential issue | 🟠 Major

Normalize and tighten credential whitespace handling.

Validation currently allows leading/trailing whitespace in otherwise non-empty values (for both sessionKey and orgId). If raw values are persisted/sent, this can pass validation but fail provider auth. Please trim at save/use boundaries and reject whitespace-containing orgId values.

🔧 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 | 🟠 Major

Localize/reset-countdown copy is still assembled from English fragments.

metaText, tooltipText, formatResetTooltip(_:), formatResetVerbose(_:), and relativeCountdown(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 / .other keys.

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: dlog calls 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 saveIndex fails after keychain deletion, the next launch would restore an account with no secret. Callers of secret(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.

keychainServiceResolver is a var that 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) returns nil, 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 dedicated SettingsLayout-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

📥 Commits

Reviewing files that changed from the base of the PR and between c0de990 and e8789f4.

⛔ Files ignored due to path filters (10)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/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

Comment thread Sources/Sidebar/ProviderAccountsFooterPanel.swift
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from e8789f4 to 7aed444 Compare April 13, 2026 21:34
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 .invalidCredentials here 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 or XCTUnwrap.

🛡️ 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 DateFormatter uses a fixed en_US_POSIX locale, 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

📥 Commits

Reviewing files that changed from the base of the PR and between e8789f4 and 7aed444.

⛔ Files ignored due to path filters (10)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/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

Comment thread Sources/Sidebar/ProviderAccountEditorSheet.swift Outdated
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from 2fb8e80 to bd98722 Compare April 14, 2026 00:06
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
Sources/Providers/ClaudeProvider.swift (2)

11-18: ⚠️ Potential issue | 🟠 Major

Normalize 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 ClaudeUsageFetcher and turns into a hard-to-diagnose 401. Please trim before persistence, or trim again before building the Cookie header.

🤖 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 | 🟡 Minor

Reject embedded whitespace in orgId.

trimmingCharacters(in:) only removes edge whitespace, so values like org id or uuid\nsuffix still 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 | 🟠 Major

The reset/countdown copy is still assembled from English fragments.

metaText, tooltipText, formatResetTooltip, formatResetVerbose, and relativeCountdown still emit raw d/h/m, in …, and <1m text. That keeps both the footer and popover reset strings out of Localizable.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 / .other keys.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7aed444 and bd98722.

⛔ Files ignored due to path filters (10)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/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

Comment thread Sources/Providers/CodexUsageFetcher.swift
Comment thread Sources/Providers/ProviderAccountsController.swift
Comment thread Sources/Providers/StatuspageIOFetcher.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch 3 times, most recently from a203aa1 to 30eabb8 Compare April 14, 2026 10:30
@tranquillum
Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 14, 2026

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@tranquillum I have started the AI code review. It will take a few minutes to complete.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== ProviderAccountStore.swift ==="
cat Sources/Providers/ProviderAccountStore.swift

echo ""
echo "=== ProviderAccountsController.swift ==="
cat Sources/Providers/ProviderAccountsController.swift

echo ""
echo "=== ClaudeUsageFetcher.swift ==="
cat Sources/Providers/ClaudeUsageFetcher.swift

echo ""
echo "=== CodexUsageFetcher.swift ==="
cat Sources/Providers/CodexUsageFetcher.swift

echo ""
echo "=== ProviderHTTP.swift ==="
cat Sources/Providers/ProviderHTTP.swift

echo ""
echo "=== UsageProvider.swift ==="
cat Sources/Providers/UsageProvider.swift

echo ""
echo "=== ProviderRegistry.swift ==="
cat Sources/Providers/ProviderRegistry.swift

echo ""
echo "=== ClaudeProvider.swift ==="
cat Sources/Providers/ClaudeProvider.swift

echo ""
echo "=== CodexProvider.swift ==="
cat Sources/Providers/CodexProvider.swift

echo ""
echo "=== StatuspageIOFetcher.swift ==="
cat Sources/Providers/StatuspageIOFetcher.swift

echo ""
echo "=== ProviderAccount.swift ==="
cat Sources/Providers/ProviderAccount.swift

echo ""
echo "=== ProviderISO8601DateParser.swift ==="
cat Sources/Providers/ProviderISO8601DateParser.swift

echo ""
echo "=== ProviderUsageColorSettings.swift ==="
cat Sources/Providers/ProviderUsageColorSettings.swift

Length of output: 52014


🏁 Script executed:

#!/bin/bash
echo "=== ProviderAccountsFooterPanel.swift ==="
cat Sources/Sidebar/ProviderAccountsFooterPanel.swift

echo ""
echo "=== ProviderAccountsPopover.swift ==="
cat Sources/Sidebar/ProviderAccountsPopover.swift

echo ""
echo "=== ProviderAccountEditorSheet.swift ==="
cat Sources/Sidebar/ProviderAccountEditorSheet.swift

echo ""
echo "=== ProviderAccountsSettingsSection.swift ==="
cat Sources/Sidebar/ProviderAccountsSettingsSection.swift

echo ""
echo "=== ProviderTests.swift ==="
cat cmuxTests/ProviderTests.swift

echo ""
echo "=== AppDelegate.swift (relevant section) ==="
grep -n -A 5 -B 2 "ProviderAccount" Sources/AppDelegate.swift

echo ""
echo "=== ContentView.swift (relevant section) ==="
grep -n -A 5 -B 2 "ProviderAccountsFooterPanel" Sources/ContentView.swift

Length of output: 65424


@tranquillum: This is a well-structured, security-conscious feature with good documentation and solid test coverage. Here are my findings, organized by severity:


🔴 High

1. Localized string keys containing runtime values (ProviderAccountsPopover.swift)

Two Text views pass runtime-interpolated strings as the key argument to String(localized:):

// Line ~175
Text(String(localized: "Updated \(formattedTime(snapshot.fetchedAt))"))

// Line ~155
Text(String(localized: "Resets \(formatResetVerbose(resetsAt))"))

String(localized:) uses its first argument as the lookup key in your .strings/.xcstrings catalog. Because the key changes at runtime (e.g. "Updated 01:43:15", "Updated 01:44:02", …), it will never match any catalog entry and falls back to the raw interpolated string in every locale. The dynamic content will also block the Xcode string extractor from cataloging these entries.

The fix is to use a stable key with a defaultValue containing the interpolation:

// Before
Text(String(localized: "Updated \(formattedTime(snapshot.fetchedAt))"))

// After
Text(String(
    localized: "providers.accounts.popover.updated",
    defaultValue: "Updated \(formattedTime(snapshot.fetchedAt))"
))
// Before
Text(String(localized: "Resets \(formatResetVerbose(resetsAt))"))

// After
Text(String(
    localized: "providers.accounts.popover.resets",
    defaultValue: "Resets \(formatResetVerbose(resetsAt))"
))

2. Synchronous Keychain access on the main thread

ProviderAccountEditorSheet.onAppear calls ProviderAccountStore.shared.secret(for:) synchronously on the main actor, which internally calls SecItemCopyMatching. Similarly, all ProviderAccountStore mutating methods (add, update, remove) run Keychain operations on the main actor.

On a freshly unlocked device or under Keychain contention, SecItem* calls can take 100 ms–500 ms. This will freeze the UI briefly but visibly when opening the editor sheet or saving/removing credentials.

Consider wrapping Keychain operations in a Task.detached or a dedicated actor, then hopping back to @MainActor to update published state:

// In ProviderAccountEditorSheet.onAppear
if let account = editingAccount {
    displayName = account.displayName
    Task.detached(priority: .userInitiated) { [account] in
        do {
            let secret = try ProviderAccountStore.shared.secret(for: account.id)
            await MainActor.run {
                for field in provider.credentialFields {
                    values[field.id] = secret.fields[field.id] ?? ""
                }
            }
        } catch { /* surface error on main actor */ }
    }
}

🟡 Medium

3. Cookie header not sanitized for semicolons (ClaudeUsageFetcher.swift)

headers: ["Cookie": "sessionKey=\(sessionKey)"]

ProviderHTTP.getJSONObject only strips newlines from header values. A sessionKey containing a semicolon (;) would inject additional cookie directives, e.g.:

Cookie: sessionKey=sk-ant-sid01-abc; Path=/; Domain=.evil.com

Since the value comes directly from the user-entered Keychain secret, exploitability is self-inflicted, but it's a defense-in-depth gap. Consider also stripping control characters and semicolons from cookie values:

// In ClaudeUsageFetcher, before passing to headers:
let sanitizedKey = sessionKey
    .components(separatedBy: .controlCharacters).joined()
    .components(separatedBy: CharacterSet(charactersIn: ";\r\n")).joined()

4. Data consistency risk in remove() (ProviderAccountStore.swift)

The current flow is:

  1. deleteSecret (Keychain) ✅
  2. saveIndex (UserDefaults) — if this fails:
  3. In-memory accounts array is still updated to remove the account
  4. On next launch, the stale index entry loads a secret-less account, causing a ProviderAccountStoreError.keychain error on every fetch tick until the user manually removes it from Settings

The comment acknowledges this, but the user-visible symptom (a ghost account that can never be refreshed) is confusing. A suggested mitigation: on reload() at startup, cross-check each index entry against the Keychain and silently prune any entries whose secret cannot be found:

private func loadIndex() -> [ProviderAccount] {
    guard let data = userDefaults.data(forKey: indexKey),
          let decoded = try? JSONDecoder().decode([ProviderAccount].self, from: data) else {
        return []
    }
    // Prune orphan index entries whose Keychain secret was deleted
    // (e.g., after a failed saveIndex during a prior remove() call)
    return decoded.filter { account in
        let service = keychainServiceResolver(account.providerId)
        return (try? loadSecret(for: account.id, service: service)) != nil
    }
}

5. Tick silently dropped if a prior fetch is still in-flight (ProviderAccountsController.swift)

private func scheduleTick(force: Bool = false) {
    guard currentTask == nil || force else { return }
    ...
}

A non-forced tick (from the 60-second timer) is silently discarded while currentTask != nil. With the 10-second HTTP timeout and potentially multiple accounts, a slow-network fetch cycle could take close to 60 seconds, causing the next scheduled tick to be dropped entirely and delaying the following refresh by up to ~2 minutes.

Consider replacing the guard with a coalescing pattern: let the timer tick enqueue a "pending" flag and re-schedule after the active task completes, rather than dropping the tick completely.


🔵 Low / Nit

6. ProviderUsageColorSettings tests write to UserDefaults.standard

ProviderUsageColorSettings.shared is a singleton backed by UserDefaults.standard. The test's setUp calls resetToDefaults(), which mitigates ordering issues, but a test crash before teardown leaves modified standard defaults. Consider having ProviderUsageColorSettings accept an injected UserDefaults (similar to ProviderAccountStore) so tests can use an isolated suite.


7. ProviderSection.emptyPlaceholder branch is unreachable in normal flow

providersWithAccounts (in ProviderAccountsFooterPanel) filters to only providers that have accounts, so ProviderSection.hasAccounts is always true when the section is instantiated. The emptyPlaceholder branch only fires if accounts are removed while the view is alive and before SwiftUI tears down the section. This is harmless but worth a comment to clarify intent.


8. Missing security-sensitive test cases

The test suite is good overall. Consider adding:

// ProviderSecret never leaks field values in description/debugDescription
func testProviderSecretDescriptionRedactsValues() {
    let secret = ProviderSecret(fields: ["sessionKey": "sk-ant-sid01-supersecret"])
    XCTAssertFalse(secret.description.contains("supersecret"))
    XCTAssertFalse(secret.debugDescription.contains("supersecret"))
    XCTAssertTrue(secret.description.contains("sessionKey"))
    XCTAssertTrue(secret.description.contains("<redacted>"))
}

// ProviderHTTP header sanitization strips newlines
func testGetJSONObjectSanitizesHeaderNewlines() {
    // Verify via URLProtocol mock that a header value with \r\n
    // does not produce a multi-line header in the outgoing request
}

9. CFBoolean identity check in doubleValue (CodexUsageFetcher.swift)

The === comparison against kCFBooleanTrue/kCFBooleanFalse relies on pointer identity of bridged singletons. This is correct on Apple platforms but can be made more explicit using CFGetTypeID:

// More explicit alternative
if CFGetTypeID(number) == CFBooleanGetTypeID() {
    return nil
}

Summary table

# 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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

isSession: false
)

Text(String(localized: "Updated \(formattedTime(snapshot.fetchedAt))"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace interpolated localization key for fetched-at label

Using String(localized: "Updated \(formattedTime(snapshot.fetchedAt))") builds a runtime lookup key that does not exist in the string catalog, so the popover falls back to English in non-English locales and translators cannot control word order around the time. This should use a stable localization key (with a placeholder) or compose the existing localized label with the formatted timestamp.

Useful? React with 👍 / 👎.

private func popoverResetText(window: ProviderUsageWindow, isSession: Bool) -> some View {
Group {
if let resetsAt = window.resetsAt {
Text(String(localized: "Resets \(formatResetVerbose(resetsAt))"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace interpolated localization key for reset label

The reset line uses String(localized: "Resets \(formatResetVerbose(resetsAt))"), which similarly creates a dynamic key that is not translatable. In localized builds this string will remain English and prevents locale-specific grammar for the reset time phrase. Use a fixed localization key with formatting or join an existing localized "Resets" label with the formatted value.

Useful? React with 👍 / 👎.

Text(account.displayName)
.font(.system(size: 13, weight: .medium))
if let snapshot = controller.snapshots[account.id] {
Text(String(localized: "Session \(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize the Session/Week usage summary format

The settings summary String(localized: "Session \(snapshot.session.utilization)% · Week \(snapshot.week.utilization)%") is also a dynamic lookup key, so it will not pick up translations and will display English text for users running other languages. This should be a stable localized format string (or assembled from localized tokens) so translators can localize both labels and punctuation/order.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread README.md Outdated
Comment thread Sources/Providers/ProviderAccountStore.swift Outdated
Comment thread cmuxTests/ProviderTests.swift Outdated
Comment thread Sources/Providers/CodexUsageFetcher.swift Outdated
let fiveHourResetsAt = ProviderISO8601DateParser.parse(fiveHour["resets_at"] as? String)

let sevenDay = json["seven_day"] as? [String: Any]
let sevenDayUtil = min(max((sevenDay?["utilization"] as? NSNumber)?.intValue ?? 0, 0), 100)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 14, 2026

Choose a reason for hiding this comment

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

P2: Weekly quota parsing silently defaults missing/invalid seven_day utilization to 0%, which can mask response/schema problems and display misleading usage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Providers/ClaudeUsageFetcher.swift, line 80:

<comment>Weekly quota parsing silently defaults missing/invalid `seven_day` utilization to 0%, which can mask response/schema problems and display misleading usage.</comment>

<file context>
@@ -0,0 +1,89 @@
+        let fiveHourResetsAt = ProviderISO8601DateParser.parse(fiveHour["resets_at"] as? String)
+
+        let sevenDay = json["seven_day"] as? [String: Any]
+        let sevenDayUtil = min(max((sevenDay?["utilization"] as? NSNumber)?.intValue ?? 0, 0), 100)
+        let sevenDayResetsAt = ProviderISO8601DateParser.parse(sevenDay?["resets_at"] as? String)
+
</file context>
Fix with Cubic

@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch 3 times, most recently from 3c7187d to e959d35 Compare April 14, 2026 15:15
@tranquillum
Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 14, 2026

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@tranquillum I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

@tranquillum: I'll kick off a fresh review of this PR right away!

✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
Sources/Providers/ProviderAccountStore.swift (1)

140-152: Consider validating keychain presence when loading the index.

loadIndex() trusts that every decoded account has a corresponding keychain secret. If an app crash or write failure leaves an orphaned index entry (account in UserDefaults but secret deleted from keychain), that entry reappears on next launch and fails only when secret(for:) is called. Pruning such orphans at load time would surface the inconsistency earlier and prevent confusion.

♻️ Suggested defensive validation
 private func loadIndex() -> [ProviderAccount] {
     guard let data = userDefaults.data(forKey: indexKey) else {
         return []
     }
     do {
-        return try JSONDecoder().decode([ProviderAccount].self, from: data)
+        let decoded = try JSONDecoder().decode([ProviderAccount].self, from: data)
+        // Prune accounts whose keychain secret is missing (crash recovery, etc.)
+        let valid = decoded.filter { account in
+            let service = account.keychainService ?? keychainServiceResolver(account.providerId)
+            var query: [CFString: Any] = [
+                kSecClass: kSecClassGenericPassword,
+                kSecAttrService: service,
+                kSecAttrAccount: account.id.uuidString,
+                kSecAttrSynchronizable: kCFBooleanFalse as Any,
+            ]
+            query[kSecReturnData] = false
+            return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess
+        }
+        if valid.count != decoded.count {
+            // Persist pruned list so orphans don't reappear
+            try? saveIndex(valid)
+        }
+        return valid
     } catch {
         NSLog("ProviderAccountStore: failed to decode account index (\(data.count) bytes): \(error)")
         return []
     }
 }

Note: This synchronous keychain check runs once at init (not on every reload) so the brief main-thread hit is acceptable, but you could also move the validation into an async Task if startup latency is a concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ProviderAccountStore.swift` around lines 140 - 152, The
loadIndex() method in ProviderAccountStore currently returns decoded
ProviderAccount entries from UserDefaults without verifying a corresponding
keychain secret, which can leave orphaned accounts; update loadIndex() to, after
decoding, iterate the accounts and call the keychain lookup used by secret(for:)
(or a lower-level Keychain API) for each account identifier, drop any accounts
whose secret lookup fails, log occurrences with NSLog including the account id
and error, and persist the pruned index back to userDefaults under indexKey so
subsequent loads won't return orphaned entries; ensure this validation runs
synchronously at init (or optionally wrap in Task if you later choose async) and
keep the function signature loadIndex() unchanged.
Sources/Providers/ProviderUsageColorSettings.swift (1)

44-60: Clamp the absolute threshold bounds in the property observers too.

setThresholds(low:high:) preserves the intended 1...99 contract, but these two @Published properties are still publicly writable and their didSet blocks only fix ordering. A future direct assignment can persist 0, 100, or other out-of-range values and skew color(for:).

Consider making lowMidThreshold / midHighThreshold private(set) and routing all writes through setThresholds(low:high:), or clamp to 1...98 / 2...99 inside didSet before saving to UserDefaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ProviderUsageColorSettings.swift` around lines 44 - 60, The
two publicly writable `@Published` properties lowMidThreshold and midHighThreshold
can still be set out of the intended 1...99 range; update them so writes are
clamped and/or routed through setThresholds(low:high:). Either make the
properties private(set) and require external code to call
setThresholds(low:high:) (which already enforces 1...99), or add clamping logic
inside each didSet (for lowMidThreshold clamp to 1...98 and for midHighThreshold
clamp to 2...99, then ensure ordering) before calling defaults.set(...). Keep
references to defaults and Self.keyLowMidThreshold / Self.keyMidHighThreshold
and ensure color(for:) sees only valid threshold values.
🤖 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/ProviderAccountsController.swift`:
- Around line 220-221: In ProviderAccountsController where you currently have
the invalid pattern `case .failure(is CancellationError):`, bind the associated
error value and narrow its type instead of using `is` in the pattern; for
example, match `case .failure(let error)` and then either use a type cast
pattern or a `where` clause to check that `error` is a CancellationError, then
call `continue`; update the switch arm that references the `.failure` enum case
and the CancellationError type accordingly.

In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 347-354: The fallback "—" in the metaText computed property is not
localized; update metaText (used by Text(metaText)) to return a localized string
via String(localized: "sidebar.providerAccounts.resetPlaceholder", defaultValue:
"—") (or change the view to render a non-text placeholder view when
window.resetsAt is nil/expired) so all user-facing strings follow the
String(localized:..., defaultValue:...) pattern; modify the metaText getter and
any callers (e.g., Text(metaText)) accordingly and add the new localization key.

---

Nitpick comments:
In `@Sources/Providers/ProviderAccountStore.swift`:
- Around line 140-152: The loadIndex() method in ProviderAccountStore currently
returns decoded ProviderAccount entries from UserDefaults without verifying a
corresponding keychain secret, which can leave orphaned accounts; update
loadIndex() to, after decoding, iterate the accounts and call the keychain
lookup used by secret(for:) (or a lower-level Keychain API) for each account
identifier, drop any accounts whose secret lookup fails, log occurrences with
NSLog including the account id and error, and persist the pruned index back to
userDefaults under indexKey so subsequent loads won't return orphaned entries;
ensure this validation runs synchronously at init (or optionally wrap in Task if
you later choose async) and keep the function signature loadIndex() unchanged.

In `@Sources/Providers/ProviderUsageColorSettings.swift`:
- Around line 44-60: The two publicly writable `@Published` properties
lowMidThreshold and midHighThreshold can still be set out of the intended 1...99
range; update them so writes are clamped and/or routed through
setThresholds(low:high:). Either make the properties private(set) and require
external code to call setThresholds(low:high:) (which already enforces 1...99),
or add clamping logic inside each didSet (for lowMidThreshold clamp to 1...98
and for midHighThreshold clamp to 2...99, then ensure ordering) before calling
defaults.set(...). Keep references to defaults and Self.keyLowMidThreshold /
Self.keyMidHighThreshold and ensure color(for:) sees only valid threshold
values.
🪄 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: 7d8af3e6-e580-4ae3-a5ad-cee1fe39d616

📥 Commits

Reviewing files that changed from the base of the PR and between 41bf5c8 and 2975726.

⛔ Files ignored due to path filters (11)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-degraded.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (8)
  • Sources/Providers/ProviderISO8601DateParser.swift
  • README.md
  • GhosttyTabs.xcodeproj/project.pbxproj
  • docs/usage-monitoring-setup.md
  • Sources/Providers/ProviderAccount.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • docs/providers.md
  • Sources/Providers/UsageProvider.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/CodexProvider.swift

Comment thread Sources/Providers/ProviderAccountsController.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsFooterPanel.swift
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue 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="Sources/Providers/ProviderAccountsController.swift">

<violation number="1" location="Sources/Providers/ProviderAccountsController.swift:156">
P2: `tick` reconciles usage/status using a stale `accounts` snapshot across `await`, which can retain removed-account/provider state until a later refresh.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Providers/ProviderAccountsController.swift
@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from 2975726 to ddaef05 Compare April 15, 2026 17:53
@tranquillum
Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 15, 2026

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@tranquillum I have started the AI code review. It will take a few minutes to complete.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

The PR description images show:

  • usage-monitoring-overview: Sidebar footer expanded — CLAUDE (Personal: Sess 12%/Week 10%, Work: Sess 100%/Week 14%) and CODEX (Personal: Sess 27%/Week 8%), each with green "Operational" dots.
  • usage-monitoring-popover-claude/codex: Per-account Sess/Week bars, exact reset countdowns, provider status, and "Refresh now" / "Manage accounts…" actions.
  • usage-monitoring-settings-filled: Settings → AI Usage Monitoring with CLAUDE (Personal/Work) and CODEX (Personal) rows (Edit/Remove), color pickers (Low=green, Mid=yellow, High=red), threshold steppers (85/95), and an "Interpolate between colors" toggle.
  • usage-monitoring-editor-claude/codex: Account editor sheets with Display name + credential fields; Codex includes jq-based extraction hints for ~/.codex/auth.json.
  • usage-monitoring-sidebar-collapsed: Compact > CLAUDE • Operational and > CODEX • Operational rows with a settings gear icon.

@tranquillum: Kicking off another review pass! As a reminder, the key open findings from my analysis in this thread are:

🔴 High:

  1. Runtime-interpolated String(localized:) keys (ProviderAccountsPopover.swift) — keys like "Updated \(formattedTime(...))" defeat the localization catalog; use stable keys with defaultValue.
  2. Synchronous Keychain SecItem* on the main thread (ProviderAccountEditorSheet.swift / ProviderAccountStore.swift) — risks 100–500 ms UI freezes; move to Task.detached or a dedicated actor.

🟡 Medium:
3. Cookie header semicolon injection (ClaudeUsageFetcher.swift) — strip ; from sessionKey before building the Cookie: header.
4. Stale ghost account on remove() split failure (ProviderAccountStore.swift) — prune orphan index entries in loadIndex() by cross-checking Keychain presence.
5. Timer ticks silently dropped (ProviderAccountsController.swift) — replace the guard currentTask == nil drop with a coalescing/pending-flag pattern.

🔵 Low/Nit:
6. ProviderUsageColorSettings tests write to UserDefaults.standard — inject a per-test suite.
7. ProviderSection.emptyPlaceholder branch unreachable in normal flow — add a clarifying comment.
8. Missing tests for ProviderSecret field-value redaction and header newline sanitization.
9. CFBoolean === identity check in CodexUsageFetcher.swiftCFGetTypeID is more idiomatic.


🧠 Learnings used
Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-02T21:40:59.098Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session snapshot logic, detected foreground command lines are only recorded if they pass SessionRestoreCommandSettings.isCommandAllowed(...). Commit f887189 moved gating to the SessionForegroundProcessCache so unallowed (potentially sensitive) commands are never written to the session JSON.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-05T21:26:10.710Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, applySessionPanelMetadata() must gate listeningPorts restoration on the per-panel snapshot.terminal?.isRemoteBacked flag (not workspace-wide remoteTerminalStartupCommand()), so that local panels are always eligible for port restore regardless of current SSH state. Fixed in commit 4d0fd871 (PR `#2545`).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2884
File: Sources/AppIconDockTilePlugin.swift:16-19
Timestamp: 2026-04-14T08:50:27.729Z
Learning: Repo: manaflow-ai/cmux — Sources/AppIconDockTilePlugin.swift — Two-layer icon persistence contract (PR `#2884`):
- App process (Sources/cmuxApp.swift): AppIconMode.automatic returns nil for imageName to avoid writing custom icons to the bundle from the app side (runtime-only swaps).
- Dock plugin (Sources/AppIconDockTilePlugin.swift): DockTileAppIconMode.automatic intentionally returns a concrete NSImage.Name (AppIconDark or AppIconLight) based on the current effective appearance, so the plugin persists an appearance-matched icon to the bundle. This ensures the Dock shows the correct icon after force-kill/quit instead of falling back to the static bundle asset.
- Do NOT flag DockTileAppIconMode.automatic returning a concrete image name as a bug; it is the intentional Dock-side persistence path. Only when the user explicitly selects light or dark mode is the plugin's behavior identical to cmuxApp.swift. The .automatic branch must NOT return nil from the plugin.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2662
File: Sources/TabManager.swift:1994-1995
Timestamp: 2026-04-07T05:27:49.028Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In applyWorkspacePullRequestRefreshResults(...), resolution .unsupportedRepository must (1) clear any existing workspace.panelPullRequests[panelId] entry and (2) reset lastTerminalState timestamp. scheduleNextWorkspacePullRequestPoll(...) sets nextPollAt to a jittered background interval for .unsupportedRepository, avoiding hot-looping `git remote -v`. Intent: when a panel moves to a non-GitHub or otherwise unsupported repo, clear the sidebar PR badge and back off polling.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:4965-4988
Timestamp: 2026-04-01T09:51:45.737Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, WorkspaceRemoteSessionController.updateRemotePortScanTTYsLocked(_:) must drop remoteScannedPortsByPanel entries when a panel’s TTY changes (compare previousTTYNames vs nextTTYNames). It should also use keepPolledRemotePortsUntilTTYScan to retain the last host-wide polling snapshot only until the first TTY-scoped scan completes, and clear it when no TTYs are tracked.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:679-709
Timestamp: 2026-04-05T20:54:35.429Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session restore, SessionTerminalPanelSnapshot now includes isRemoteBacked. createPanel(from:inPane:) uses this per-panel flag (panelWasRemoteBacked) to: (1) pass initialInput only for local panels, and (2) persist panelRestoreCommands only for local panels. Remote-backed terminals also omit restore/detected commands and have listeningPorts [] in the snapshot.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:8178-8197
Timestamp: 2026-04-01T22:46:08.900Z
Learning: Repo: manaflow-ai/cmux — Do not persist remote-detected listening ports. In Sources/Workspace.swift, sessionPanelSnapshot must emit [] for listeningPorts when a panel is remote (panelId ∈ remoteDetectedSurfaceIds or isRemoteTerminalSurface), and applySessionPanelMetadata must not restore remote ports. Remote detections remain in-memory only; covered by SessionPersistenceTests.testSessionSnapshotSkipsTransientRemoteListeningPorts.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: CLI/cmux.swift:462-499
Timestamp: 2026-03-09T02:08:54.956Z
Learning: Repo: manaflow-ai/cmux
PR: `#1089`
File: CLI/cmux.swift
Component: ClaudeHookTagExtractor.extractTags(subtitle:body:)
Learning: For Claude Code session tag extraction, pre-redact sensitive spans (UUIDs, emails, access tokens, filesystem paths, ENV_VAR=..., long numerics) across the combined body+subtitle using unanchored sensitiveSpanPatterns before tokenization. Then tokenize and still filter each token with anchored sensitivePatterns. Rationale: prevents PII/path fragments from slipping into searchable tags after delimiter splitting.

Learnt from: lucasward
Repo: manaflow-ai/cmux PR: 1903
File: Sources/ContentView.swift:13069-13113
Timestamp: 2026-03-21T06:23:38.764Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, DraggableFolderNSView.updateIcon() intentionally applies the "sidebarMonochromeIcons" setting only on view creation (no live observer). Do not add defaults observers/AppStorage for live refresh in feature-scoped PRs; a live-refresh can be considered in a separate follow-up.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/AppDelegate.swift:2196-2200
Timestamp: 2026-04-03T03:36:45.112Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate.swift, when KeyboardShortcutSettings.didChangeNotification fires, AppDelegate must clear configured-chord caches (pendingConfiguredShortcutChord and activeConfiguredShortcutChordPrefixForCurrentEvent) via clearConfiguredShortcutChordState() before refreshing tooltips/UI. Also clear chord state on applicationWillResignActive to avoid cross-activity leakage. Verified by cmuxTests/AppDelegateShortcutRoutingTests.swift::testShortcutChangeClearsPendingConfiguredChord.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/CmuxConfigExecutor.swift:107-118
Timestamp: 2026-04-14T19:56:42.696Z
Learning: Repo: manaflow-ai/cmux — In Sources/CmuxConfigExecutor.swift, executeWorkspaceCommand for wsDef.target == .current intentionally uses closePanel(panelId, force: true) before calling applyCustomLayout. This is by design: soft-closing (force: false) leaves detached panes that applyCustomLayout cannot reconcile. If a non-destructive autoApply variant is needed in the future, it should be opted into explicitly via a separate flag rather than changing the default force: true behavior.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: Sources/Workspace.swift:1072-1075
Timestamp: 2026-04-04T02:21:20.453Z
Learning: Repo: manaflow-ai/cmux — Foreground-auth deferral: In CLI/cmux.swift, PermitLocalCommand and LocalCommand are injected only into the foreground startup SSH command (e.g., via deferredRemoteReconnectLocalCommand(...)) and are not added to the ssh_options payload sent in workspace.remote.configure. Consequently, WorkspaceRemoteSSHBatchCommandBuilder.batchArguments(...) and WorkspaceRemoteSessionController.backgroundSSHOptions(_:) do not need to filter LocalCommand/PermitLocalCommand for batch operations.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-01T09:50:23.728Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift within CMUXCLI.buildInteractiveRemoteShellScript(...), never export CMUX_TAB_ID from the workspace UUID. CMUX_TAB_ID must be surface-scoped: only set it when a surface ID is available (map CMUX_TAB_ID to CMUX_SURFACE_ID). Rationale: tab-action/rename-tab resolve CMUX_TAB_ID before CMUX_SURFACE_ID; workspace-scoped values misroute or fail.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, `claudeTeamsLaunchArguments` must use two independent branches: (1) prepend `--teammate-mode auto` only when the caller has NOT already supplied `--teammate-mode`; (2) append `--settings <claudeHooksJSON>` unconditionally (modulo `CMUX_CLAUDE_HOOKS_DISABLED=1` opt-out). These two branches must never be coupled in a single early-return or conditional block — coupling them was the original `#2229` regression (hook JSON silently bypassed when `--teammate-mode auto|manual` was explicit). Mirrors `Resources/bin/claude:208`: `exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"` where `$@` content never gates hook injection.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9691-9711
Timestamp: 2026-04-01T22:58:26.254Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (runClaudeTeams), custom Claude path resolution now trims whitespace and rejects paths that point to the cmux wrapper using isCmuxClaudeWrapper(), before falling back to PATH/bundled. The Resources/bin/claude wrapper also resolves the real path and compares against itself, requiring -f/-x to avoid recursion/self-reference.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9698-9706
Timestamp: 2026-04-01T23:08:19.526Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (CMUXCLI.runClaudeTeams), custom Claude path candidates (CMUX_CUSTOM_CLAUDE_PATH and UserDefaults key "claudeCodeCustomClaudePath") now reject directory-valued paths by checking isDirectory == false before isExecutableFile, aligning with the Resources/bin/claude wrapper’s [[ -f && -x ]] validation and ensuring fallback to PATH/bundled.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2492
File: CLI/cmux.swift:319-321
Timestamp: 2026-04-06T09:45:32.018Z
Learning: Repo: manaflow-ai/cmux — Claude hook stop classification precedence: in CLI/cmux.swift, summarizeClaudeHookStop should detect failures by checking (1) hook_event_name == "StopFailure" or (2) presence of the "error" field; only then fall back to stop_reason != "end_turn" for back-compat. The notification subtitle/body should use String(localized:) with keys claude.hook.stop.verb.stopped/completed and claude.hook.stop.state.stopped/completed.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift `runClaudeTeams`, `setenv("CMUX_CLAUDE_PID", ...)` is set unconditionally before the passthrough-subcommand check, mirroring `Resources/bin/claude:188` which exports `CMUX_CLAUDE_PID=$$` before the subcommand `case` statement. This is intentional design: the env var is dead for `mcp`/`config`/`api-key`/`rc`/`remote-control` paths but harmless, and matching the bash wrapper unconditional export exactly is a deliberate design principle for the claude-teams hook injection feature.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: Sources/GhosttyTerminalView.swift:3759-3761
Timestamp: 2026-04-01T22:57:41.165Z
Learning: Repo: manaflow-ai/cmux — In Sources/cmuxApp.swift, ClaudeCodeIntegrationSettings.customClaudePath(defaults:) trims surrounding whitespace and returns nil for empty/whitespace-only values; callers (e.g., TerminalSurface.createSurface(for:)) can safely set CMUX_CUSTOM_CLAUDE_PATH without additional trimming.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2043
File: Resources/bin/claude:0-0
Timestamp: 2026-04-03T07:49:14.139Z
Learning: Repo: manaflow-ai/cmux — In Resources/bin/claude, SELF_DIR is validated with a broad regex `[\"\\' $\`!;|&<>()*?\[\]]` before interpolating it into HOOKS_JSON command strings. Paths that match (unsafe chars including spaces and shell metacharacters) cause the wrapper to fall back to BASE_HOOKS_JSON and emit a warning. This guard-and-fallback is the intentional design; do not suggest adding shell quoting inside the JSON command strings — the guard approach is preferred.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:4036-4038
Timestamp: 2026-04-01T09:50:41.471Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (buildInteractiveRemoteShellScript), remote shells intentionally export CMUX_SOCKET_PATH and CMUX_SOCKET as "127.0.0.1:<relayPort>" and prepend PATH with "$HOME/.cmux/bin"; CMUX_BUNDLED_CLI_PATH points to that wrapper. WorkspaceRemoteSessionController.remoteCLIWrapperScript() detects host:port, reads "~/.cmux/relay/<port>.daemon_path", and delegates to cmuxd-remote. Do not flag the non-UNIX value of CMUX_SOCKET_PATH/CMUX_SOCKET as a bug; it is the relay contract for remote sessions.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/ContentView.swift:6055-6057
Timestamp: 2026-03-09T02:09:08.742Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), within CommandPaletteSwitcherSearchIndexer.metadataKeywordsForSearch(_:detail:), when tags are present, only add "tag" and "topic" to contextKeywords; do not append "claude" unconditionally to avoid false-positive switcher matches.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:27.511Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.

Learnt from: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:37.952Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:) is shared command‑palette infrastructure across all submenus. Do not change its sync‑seeding behavior within feature‑scoped PRs; treat brief initial flashes as consistent with existing submenus. Any UX improvement (e.g., synchronous seeding on forced corpus refresh) should be implemented and tested globally in a dedicated follow‑up PR.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:12:42.553Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift and Sources/Panels/BrowserPanel.swift, BiDi override (U+202A–202E, U+2066–2069) and zero-width char (U+200B–200F, U+FEFF) filtering is implemented via a shared `dangerousScalars: Set<UInt32>` in each class. v2SanitizeWebText() truncates to 200 chars; v2SanitizeXPath() caps at 2000 chars for selector fidelity. Both use a shared v2SanitizeScalar() predicate. BrowserPickerMessageHandler.sanitize() uses the same dangerousScalars pattern with a 200-char cap.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-08T03:36:30.160Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.FileBrowserDrawerState threading pattern (PR `#1909`, commit e0e57809): FileBrowserDrawerState must be threaded through AppDelegate.configure() as a weak stored property (matching the sidebarState pattern), passed through both configure() call sites, with registerMainWindow parameter made non-optional. The fallback `?? FileBrowserDrawerState()` must NOT be used as it creates detached instances that are not properly owned by the window context.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-3021
Timestamp: 2026-04-14T20:00:16.490Z
Learning: Repo: manaflow-ai/cmux — Accessibility convention: For toolbar actions in macOS 26+ SwiftUI .toolbar (Sources/ContentView.swift), use localized .accessibilityLabel (and identifiers) for VoiceOver; AppKit NSToolbarItems in Sources/WindowToolbarController.swift set localized label/toolTip/accessibilityDescription. Label(...).labelStyle(.iconOnly) is not required here as VO picks up the localized accessibilityLabel.

Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 2262
File: Resources/Localizable.xcstrings:84820-84835
Timestamp: 2026-03-27T22:00:27.420Z
Learning: Repo: manaflow-ai/cmux — In `Resources/Localizable.xcstrings`, it is intentional for lower-confidence locales (e.g., ar, bs, da, pl, th, km, tr) to carry English-language fallback `stringUnit.value` strings with `state: "translated"` for newly added keys. This matches the existing `.xcstrings` catalog convention for the project; do not flag English values with state "translated" in these locales as a localization bug or suggest changing the state to "needs review" / "untranslated".

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 2505
File: Sources/AppDelegate.swift:5636-5642
Timestamp: 2026-04-06T07:18:41.310Z
Learning: Repo: manaflow-ai/cmux — In AppDelegate’s .keyDown focus-repair path, never dereference NSTextView.delegate (unsafe-unretained). Resolve field-editor ownership via cmuxFieldEditorOwnerView(_), and prefer superview/nextResponder traversal or hostedView.responderMatchesPreferredKeyboardFocus(...) for matching, as applied in AppDelegate.swift and GhosttySurfaceScrollView.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:01:33.134Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/BrowserPanel.swift, BrowserPickerMessageHandler gates WKScriptMessage delivery via NSEvent.addLocalMonitorForEvents(.leftMouseDown): on Option+click it stamps ProcessInfo.processInfo.systemUptime into lastNativeOptionClickTime; userContentController(_:didReceive:) rejects postMessages where elapsed >= 1.0s and zeroes the timestamp after acceptance (one-shot). The NSEvent monitor is removed in BrowserPickerMessageHandler.deinit. BrowserPanel.bindWebView(_:) removes the old script message handler and nils pickerMessageHandler before re-registering (prevents monitor leaks on profile change/webView replacement). BrowserPanel.deinit also removes the "cmuxPointer" script message handler.
✅ Actions performed

Review triggered.

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

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ 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".

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue 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="Sources/Sidebar/ProviderAccountsPopover.swift">

<violation number="1" location="Sources/Sidebar/ProviderAccountsPopover.swift:279">
P2: Session popover incorrectly labels stale past-reset snapshots as "Session not started," conflating stale data with true unstarted sessions.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
docs/usage-monitoring-setup.md (1)

103-107: Clarify field key vs UI label for Claude org identifier.

At Line 106, consider documenting this as “orgId (shown as Organization ID in UI)” to keep terminology consistent with troubleshooting text (Line 150) and reduce ambiguity for users cross-referencing logs/config keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/usage-monitoring-setup.md` around lines 103 - 107, Update the
documentation to disambiguate the Claude organization identifier by changing the
Credentials bullet to list "orgId (shown as **Organization ID** in UI)" instead
of just "Organization ID", keep the existing `sessionKey` entry and the Keychain
service `com.cmuxterm.app.claude-accounts`, and ensure the term `orgId` matches
the troubleshooting section wording so readers can cross-reference logs/config
keys with the UI label.
Sources/Providers/ClaudeUsageFetcher.swift (1)

122-128: Consider using CFGetTypeID for CFBoolean detection.

The pointer identity check (===) works correctly on Apple platforms but CFGetTypeID is more explicit and portable:

if CFGetTypeID(number) == CFBooleanGetTypeID() {
    return nil
}

This is a minor stylistic preference — the current implementation is functionally correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ClaudeUsageFetcher.swift` around lines 122 - 128, Replace
the pointer-identity CFBoolean check in intUtilization(_:) with an explicit
CoreFoundation type check: detect CFBoolean by comparing CFGetTypeID(number) to
CFBooleanGetTypeID() and return nil when they match; keep the guard casting to
NSNumber and the final range clamp using min(max(number.intValue, 0), 100)
unchanged so only the boolean-detection line is modified.
Sources/Sidebar/ProviderAccountsSettingsSection.swift (1)

354-366: Preview strip rendering could be optimized.

Rendering 101 individual Rectangle views works but may cause unnecessary view updates. Consider using a Canvas or Path for better performance if users report lag when adjusting thresholds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Sidebar/ProviderAccountsSettingsSection.swift` around lines 354 -
366, The preview strip currently creates 101 individual Rectangle views inside
the VStack/ForEach loop (see ForEach and Rectangle and
colorSettings.color(for:)), which can cause excessive view updates; replace that
with a single drawing-backed view such as Canvas or a custom Shape/Path that
samples colorSettings.color(for:) across the 0...100 range and paints the strip
in one draw pass (or build a single horizontal gradient if colors are smoothly
varying) so updates only invalidate the Canvas rather than 101 child views; keep
the same frame/clipShape and caption layout (VStack/Text) and ensure the Canvas
uses the same color sampling function colorSettings.color(for:) to preserve
appearance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/usage-monitoring-setup.md`:
- Around line 103-107: Update the documentation to disambiguate the Claude
organization identifier by changing the Credentials bullet to list "orgId (shown
as **Organization ID** in UI)" instead of just "Organization ID", keep the
existing `sessionKey` entry and the Keychain service
`com.cmuxterm.app.claude-accounts`, and ensure the term `orgId` matches the
troubleshooting section wording so readers can cross-reference logs/config keys
with the UI label.

In `@Sources/Providers/ClaudeUsageFetcher.swift`:
- Around line 122-128: Replace the pointer-identity CFBoolean check in
intUtilization(_:) with an explicit CoreFoundation type check: detect CFBoolean
by comparing CFGetTypeID(number) to CFBooleanGetTypeID() and return nil when
they match; keep the guard casting to NSNumber and the final range clamp using
min(max(number.intValue, 0), 100) unchanged so only the boolean-detection line
is modified.

In `@Sources/Sidebar/ProviderAccountsSettingsSection.swift`:
- Around line 354-366: The preview strip currently creates 101 individual
Rectangle views inside the VStack/ForEach loop (see ForEach and Rectangle and
colorSettings.color(for:)), which can cause excessive view updates; replace that
with a single drawing-backed view such as Canvas or a custom Shape/Path that
samples colorSettings.color(for:) across the 0...100 range and paints the strip
in one draw pass (or build a single horizontal gradient if colors are smoothly
varying) so updates only invalidate the Canvas rather than 101 child views; keep
the same frame/clipShape and caption layout (VStack/Text) and ensure the Canvas
uses the same color sampling function colorSettings.color(for:) to preserve
appearance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a23a028-9d65-468b-abcf-6dab7d0af792

📥 Commits

Reviewing files that changed from the base of the PR and between 2975726 and ddaef05.

⛔ Files ignored due to path filters (11)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-degraded.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (9)
  • README.md
  • Sources/Providers/ProviderISO8601DateParser.swift
  • docs/providers.md
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • cmuxTests/ProviderTests.swift
  • Sources/ContentView.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/Providers/ProviderRegistry.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderUsageColorSettings.swift

@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from ddaef05 to edb8f75 Compare April 15, 2026 20:00
@tranquillum
Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 15, 2026

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@tranquillum I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

The PR description images show the same well-polished UI as in prior passes — the expanded sidebar footer (CLAUDE: Personal Sess 12%/Week 10%, Work Sess 100%/Week 14%; CODEX: Personal Sess 27%/Week 8%), the per-provider popovers with reset countdowns and status indicators, the Settings → AI Usage Monitoring card with account rows and color-threshold configuration, the Claude/Codex account editor sheets, and the collapsed > CLAUDE • Operational / > CODEX • Operational view.

@tranquillum: Kicking off another review pass! For reference, here is the current status of the findings from my earlier analysis, with two corrections based on closer reading of the code summaries:

🔴 High (open):

  1. Runtime-interpolated String(localized:) keys (ProviderAccountsPopover.swift) — "Updated \(formattedTime(...))" as a key defeats the catalog; use a stable key + defaultValue.
  2. Synchronous Keychain SecItem* on the main thread (ProviderAccountEditorSheet.swift / ProviderAccountStore.swift) — risks 100–500 ms UI freezes; move to Task.detached or a dedicated actor.

🟡 Medium (open):
3. Cookie header semicolon injectionLikely already addressed: ProviderHTTP.sanitizeHeaderValue strips ; and , before setting headers in getJSONObject, so the Cookie value is sanitized at the HTTP layer.
4. Stale ghost account on remove() split failure (ProviderAccountStore.swift) — prune orphan index entries in loadIndex() by cross-checking Keychain presence.
5. Timer ticks silently dropped (ProviderAccountsController.swift) — replace the guard currentTask == nil drop with a coalescing/pending-flag pattern.

🔵 Low/Nit:
6. ProviderUsageColorSettings tests write to UserDefaults.standard — inject a per-test suite for isolation.
7. ProviderSection.emptyPlaceholder branch unreachable in normal flow — add a clarifying comment.
8. Missing ProviderSecret redaction testsAlready addressed: ProviderSecretRedactionTests in cmuxTests/ProviderTests.swift covers description, debugDescription, Swift.dump, and string interpolation.
9. CFBoolean === identity check in CodexUsageFetcher.swiftCFGetTypeID is more idiomatic.

The two highest-priority items to address before merge remain #1 (localization keys) and #2 (Keychain I/O on the main thread).


🧠 Learnings used
Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-02T21:40:59.098Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session snapshot logic, detected foreground command lines are only recorded if they pass SessionRestoreCommandSettings.isCommandAllowed(...). Commit f887189 moved gating to the SessionForegroundProcessCache so unallowed (potentially sensitive) commands are never written to the session JSON.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-05T21:26:10.710Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, applySessionPanelMetadata() must gate listeningPorts restoration on the per-panel snapshot.terminal?.isRemoteBacked flag (not workspace-wide remoteTerminalStartupCommand()), so that local panels are always eligible for port restore regardless of current SSH state. Fixed in commit 4d0fd871 (PR `#2545`).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2884
File: Sources/AppIconDockTilePlugin.swift:16-19
Timestamp: 2026-04-14T08:50:27.729Z
Learning: Repo: manaflow-ai/cmux — Sources/AppIconDockTilePlugin.swift — Two-layer icon persistence contract (PR `#2884`):
- App process (Sources/cmuxApp.swift): AppIconMode.automatic returns nil for imageName to avoid writing custom icons to the bundle from the app side (runtime-only swaps).
- Dock plugin (Sources/AppIconDockTilePlugin.swift): DockTileAppIconMode.automatic intentionally returns a concrete NSImage.Name (AppIconDark or AppIconLight) based on the current effective appearance, so the plugin persists an appearance-matched icon to the bundle. This ensures the Dock shows the correct icon after force-kill/quit instead of falling back to the static bundle asset.
- Do NOT flag DockTileAppIconMode.automatic returning a concrete image name as a bug; it is the intentional Dock-side persistence path. Only when the user explicitly selects light or dark mode is the plugin's behavior identical to cmuxApp.swift. The .automatic branch must NOT return nil from the plugin.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2662
File: Sources/TabManager.swift:1994-1995
Timestamp: 2026-04-07T05:27:49.028Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In applyWorkspacePullRequestRefreshResults(...), resolution .unsupportedRepository must (1) clear any existing workspace.panelPullRequests[panelId] entry and (2) reset lastTerminalState timestamp. scheduleNextWorkspacePullRequestPoll(...) sets nextPollAt to a jittered background interval for .unsupportedRepository, avoiding hot-looping `git remote -v`. Intent: when a panel moves to a non-GitHub or otherwise unsupported repo, clear the sidebar PR badge and back off polling.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:4965-4988
Timestamp: 2026-04-01T09:51:45.737Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, WorkspaceRemoteSessionController.updateRemotePortScanTTYsLocked(_:) must drop remoteScannedPortsByPanel entries when a panel’s TTY changes (compare previousTTYNames vs nextTTYNames). It should also use keepPolledRemotePortsUntilTTYScan to retain the last host-wide polling snapshot only until the first TTY-scoped scan completes, and clear it when no TTYs are tracked.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:679-709
Timestamp: 2026-04-05T20:54:35.429Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session restore, SessionTerminalPanelSnapshot now includes isRemoteBacked. createPanel(from:inPane:) uses this per-panel flag (panelWasRemoteBacked) to: (1) pass initialInput only for local panels, and (2) persist panelRestoreCommands only for local panels. Remote-backed terminals also omit restore/detected commands and have listeningPorts [] in the snapshot.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:8178-8197
Timestamp: 2026-04-01T22:46:08.900Z
Learning: Repo: manaflow-ai/cmux — Do not persist remote-detected listening ports. In Sources/Workspace.swift, sessionPanelSnapshot must emit [] for listeningPorts when a panel is remote (panelId ∈ remoteDetectedSurfaceIds or isRemoteTerminalSurface), and applySessionPanelMetadata must not restore remote ports. Remote detections remain in-memory only; covered by SessionPersistenceTests.testSessionSnapshotSkipsTransientRemoteListeningPorts.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: CLI/cmux.swift:462-499
Timestamp: 2026-03-09T02:08:54.956Z
Learning: Repo: manaflow-ai/cmux
PR: `#1089`
File: CLI/cmux.swift
Component: ClaudeHookTagExtractor.extractTags(subtitle:body:)
Learning: For Claude Code session tag extraction, pre-redact sensitive spans (UUIDs, emails, access tokens, filesystem paths, ENV_VAR=..., long numerics) across the combined body+subtitle using unanchored sensitiveSpanPatterns before tokenization. Then tokenize and still filter each token with anchored sensitivePatterns. Rationale: prevents PII/path fragments from slipping into searchable tags after delimiter splitting.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/AppDelegate.swift:2196-2200
Timestamp: 2026-04-03T03:36:45.112Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate.swift, when KeyboardShortcutSettings.didChangeNotification fires, AppDelegate must clear configured-chord caches (pendingConfiguredShortcutChord and activeConfiguredShortcutChordPrefixForCurrentEvent) via clearConfiguredShortcutChordState() before refreshing tooltips/UI. Also clear chord state on applicationWillResignActive to avoid cross-activity leakage. Verified by cmuxTests/AppDelegateShortcutRoutingTests.swift::testShortcutChangeClearsPendingConfiguredChord.

Learnt from: lucasward
Repo: manaflow-ai/cmux PR: 1903
File: Sources/ContentView.swift:13069-13113
Timestamp: 2026-03-21T06:23:38.764Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, DraggableFolderNSView.updateIcon() intentionally applies the "sidebarMonochromeIcons" setting only on view creation (no live observer). Do not add defaults observers/AppStorage for live refresh in feature-scoped PRs; a live-refresh can be considered in a separate follow-up.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/CmuxConfigExecutor.swift:107-118
Timestamp: 2026-04-14T19:56:42.696Z
Learning: Repo: manaflow-ai/cmux — In Sources/CmuxConfigExecutor.swift, executeWorkspaceCommand for wsDef.target == .current intentionally uses closePanel(panelId, force: true) before calling applyCustomLayout. This is by design: soft-closing (force: false) leaves detached panes that applyCustomLayout cannot reconcile. If a non-destructive autoApply variant is needed in the future, it should be opted into explicitly via a separate flag rather than changing the default force: true behavior.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: Sources/Workspace.swift:1072-1075
Timestamp: 2026-04-04T02:21:20.453Z
Learning: Repo: manaflow-ai/cmux — Foreground-auth deferral: In CLI/cmux.swift, PermitLocalCommand and LocalCommand are injected only into the foreground startup SSH command (e.g., via deferredRemoteReconnectLocalCommand(...)) and are not added to the ssh_options payload sent in workspace.remote.configure. Consequently, WorkspaceRemoteSSHBatchCommandBuilder.batchArguments(...) and WorkspaceRemoteSessionController.backgroundSSHOptions(_:) do not need to filter LocalCommand/PermitLocalCommand for batch operations.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-01T09:50:23.728Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift within CMUXCLI.buildInteractiveRemoteShellScript(...), never export CMUX_TAB_ID from the workspace UUID. CMUX_TAB_ID must be surface-scoped: only set it when a surface ID is available (map CMUX_TAB_ID to CMUX_SURFACE_ID). Rationale: tab-action/rename-tab resolve CMUX_TAB_ID before CMUX_SURFACE_ID; workspace-scoped values misroute or fail.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, `claudeTeamsLaunchArguments` must use two independent branches: (1) prepend `--teammate-mode auto` only when the caller has NOT already supplied `--teammate-mode`; (2) append `--settings <claudeHooksJSON>` unconditionally (modulo `CMUX_CLAUDE_HOOKS_DISABLED=1` opt-out). These two branches must never be coupled in a single early-return or conditional block — coupling them was the original `#2229` regression (hook JSON silently bypassed when `--teammate-mode auto|manual` was explicit). Mirrors `Resources/bin/claude:208`: `exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"` where `$@` content never gates hook injection.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9691-9711
Timestamp: 2026-04-01T22:58:26.254Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (runClaudeTeams), custom Claude path resolution now trims whitespace and rejects paths that point to the cmux wrapper using isCmuxClaudeWrapper(), before falling back to PATH/bundled. The Resources/bin/claude wrapper also resolves the real path and compares against itself, requiring -f/-x to avoid recursion/self-reference.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9698-9706
Timestamp: 2026-04-01T23:08:19.526Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (CMUXCLI.runClaudeTeams), custom Claude path candidates (CMUX_CUSTOM_CLAUDE_PATH and UserDefaults key "claudeCodeCustomClaudePath") now reject directory-valued paths by checking isDirectory == false before isExecutableFile, aligning with the Resources/bin/claude wrapper’s [[ -f && -x ]] validation and ensuring fallback to PATH/bundled.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2492
File: CLI/cmux.swift:319-321
Timestamp: 2026-04-06T09:45:32.018Z
Learning: Repo: manaflow-ai/cmux — Claude hook stop classification precedence: in CLI/cmux.swift, summarizeClaudeHookStop should detect failures by checking (1) hook_event_name == "StopFailure" or (2) presence of the "error" field; only then fall back to stop_reason != "end_turn" for back-compat. The notification subtitle/body should use String(localized:) with keys claude.hook.stop.verb.stopped/completed and claude.hook.stop.state.stopped/completed.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift `runClaudeTeams`, `setenv("CMUX_CLAUDE_PID", ...)` is set unconditionally before the passthrough-subcommand check, mirroring `Resources/bin/claude:188` which exports `CMUX_CLAUDE_PID=$$` before the subcommand `case` statement. This is intentional design: the env var is dead for `mcp`/`config`/`api-key`/`rc`/`remote-control` paths but harmless, and matching the bash wrapper unconditional export exactly is a deliberate design principle for the claude-teams hook injection feature.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: Sources/GhosttyTerminalView.swift:3759-3761
Timestamp: 2026-04-01T22:57:41.165Z
Learning: Repo: manaflow-ai/cmux — In Sources/cmuxApp.swift, ClaudeCodeIntegrationSettings.customClaudePath(defaults:) trims surrounding whitespace and returns nil for empty/whitespace-only values; callers (e.g., TerminalSurface.createSurface(for:)) can safely set CMUX_CUSTOM_CLAUDE_PATH without additional trimming.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2043
File: Resources/bin/claude:0-0
Timestamp: 2026-04-03T07:49:14.139Z
Learning: Repo: manaflow-ai/cmux — In Resources/bin/claude, SELF_DIR is validated with a broad regex `[\"\\' $\`!;|&<>()*?\[\]]` before interpolating it into HOOKS_JSON command strings. Paths that match (unsafe chars including spaces and shell metacharacters) cause the wrapper to fall back to BASE_HOOKS_JSON and emit a warning. This guard-and-fallback is the intentional design; do not suggest adding shell quoting inside the JSON command strings — the guard approach is preferred.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:4036-4038
Timestamp: 2026-04-01T09:50:41.471Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (buildInteractiveRemoteShellScript), remote shells intentionally export CMUX_SOCKET_PATH and CMUX_SOCKET as "127.0.0.1:<relayPort>" and prepend PATH with "$HOME/.cmux/bin"; CMUX_BUNDLED_CLI_PATH points to that wrapper. WorkspaceRemoteSessionController.remoteCLIWrapperScript() detects host:port, reads "~/.cmux/relay/<port>.daemon_path", and delegates to cmuxd-remote. Do not flag the non-UNIX value of CMUX_SOCKET_PATH/CMUX_SOCKET as a bug; it is the relay contract for remote sessions.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/ContentView.swift:6055-6057
Timestamp: 2026-03-09T02:09:08.742Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), within CommandPaletteSwitcherSearchIndexer.metadataKeywordsForSearch(_:detail:), when tags are present, only add "tag" and "topic" to contextKeywords; do not append "claude" unconditionally to avoid false-positive switcher matches.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-06T18:23:54.205Z
Learning: Repo: manaflow-ai/cmux — Sources/Workspace.swift: When promoting an existing local terminal to remote-backed in Workspace.seedInitialRemoteTerminalSessionIfNeeded(_), clear both surfaceTTYNames[panelId] and surfaceListeningPorts[panelId] before calling trackRemoteTerminalSurface(_:) to avoid stale local TTY/port metadata in the workspace UI.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-08T03:36:30.160Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.FileBrowserDrawerState threading pattern (PR `#1909`, commit e0e57809): FileBrowserDrawerState must be threaded through AppDelegate.configure() as a weak stored property (matching the sidebarState pattern), passed through both configure() call sites, with registerMainWindow parameter made non-optional. The fallback `?? FileBrowserDrawerState()` must NOT be used as it creates detached instances that are not properly owned by the window context.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:27.511Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:01:33.134Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/BrowserPanel.swift, BrowserPickerMessageHandler gates WKScriptMessage delivery via NSEvent.addLocalMonitorForEvents(.leftMouseDown): on Option+click it stamps ProcessInfo.processInfo.systemUptime into lastNativeOptionClickTime; userContentController(_:didReceive:) rejects postMessages where elapsed >= 1.0s and zeroes the timestamp after acceptance (one-shot). The NSEvent monitor is removed in BrowserPickerMessageHandler.deinit. BrowserPanel.bindWebView(_:) removes the old script message handler and nils pickerMessageHandler before re-registering (prevents monitor leaks on profile change/webView replacement). BrowserPanel.deinit also removes the "cmuxPointer" script message handler.
✅ Actions performed

Review triggered.

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

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ 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".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (1)
Sources/Sidebar/ProviderAccountsFooterPanel.swift (1)

446-463: ⚠️ Potential issue | 🟠 Major

Localize the full tooltip/reset phrases, not just the fragments.

These branches still compose sentence order in Swift ("\(label) \(percentText) · ...", "Today \(time)"). That keeps English punctuation/word order baked into the UI and prevents translators from reordering the dynamic pieces cleanly.

♻️ Suggested fix
     private var tooltipText: String {
         let percentText = String(
             localized: "providers.accounts.usage.percent",
             defaultValue: "\(percent)%"
         )
-        var text = "\(label) \(percentText)"
+        let summary = String(
+            localized: "providers.accounts.usage.summary",
+            defaultValue: "\(label) \(percentText)"
+        )
         // Only present the reset phrase for an upcoming reset. A resetsAt in
         // the past means the server-side window rolled over but we haven't
         // fetched a refresh yet; pairing it with "resets <time>" would imply
         // a future event that has already passed.
         if let resetsAt = window.resetsAt, resetsAt > Date() {
             let resetTime = formatResetTooltip(resetsAt)
-            let resetsPrefix = String(localized: "providers.accounts.usage.resets", defaultValue: "resets")
-            text += " · \(resetsPrefix) \(resetTime)"
+            return String(
+                localized: "providers.accounts.usage.summaryWithReset",
+                defaultValue: "\(summary) · resets \(resetTime)"
+            )
         } else if percent == 0 {
-            text += " · \(String(localized: "providers.accounts.usage.notStarted", defaultValue: "not started"))"
+            return String(
+                localized: "providers.accounts.usage.summaryNotStarted",
+                defaultValue: "\(summary) · not started"
+            )
         }
-        return text
+        return summary
     }
@@
     let absolute: String
     if calendar.isDateInToday(resetsAt) {
-        absolute = "\(String(localized: "providers.accounts.popover.today", defaultValue: "Today")) \(time)"
+        absolute = String(
+            localized: "providers.accounts.popover.todayAtTime",
+            defaultValue: "Today \(time)"
+        )
     } else if calendar.isDateInTomorrow(resetsAt) {
-        absolute = "\(String(localized: "providers.accounts.popover.tomorrow", defaultValue: "Tomorrow")) \(time)"
+        absolute = String(
+            localized: "providers.accounts.popover.tomorrowAtTime",
+            defaultValue: "Tomorrow \(time)"
+        )
     } else {
         absolute = resetAbsoluteFormatter.string(from: resetsAt)
     }

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

Also applies to: 539-545

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift` around lines 446 - 463,
The tooltip currently concatenates localized fragments (percentText,
resetsPrefix) with Swift string interpolation in tooltipText, preventing
translators from reordering phrases; change this to use fully localized format
strings instead: replace the composed lines in tooltipText with calls to
String(localized: ...) that accept positional placeholders (e.g. a key like
"providers.accounts.usage.tooltip" with placeholders for label, percent, and
optional reset time, and another key for the "not started" variant), pass label,
percentText (or resetTime from formatResetTooltip) into those localized format
strings, and remove inline concatenation; ensure formatResetTooltip still
returns a localized time string but does not bake in surrounding words so the
final localization keys control full sentence order.
🧹 Nitpick comments (4)
Sources/Providers/ProviderUsageColorSettings.swift (1)

188-197: Silent fallback to black on color space conversion failure.

If NSColor(self).usingColorSpace(.sRGB) returns nil (e.g., for pattern or catalog colors), rgbComponents silently returns (0, 0, 0). This could produce unexpected black interpolation results. Consider logging a warning in DEBUG builds or documenting this behavior.

Proposed fix
     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, falling back to black")
+            `#endif`
             return (red: 0, green: 0, blue: 0)
         }
         return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ProviderUsageColorSettings.swift` around lines 188 - 197,
The rgbComponents getter silently returns black when
NSColor(self).usingColorSpace(.sRGB) is nil; change it to log a warning in debug
builds and preserve current behavior in release: wrap the nil branch in a `#if`
DEBUG block and emit a clear warning (e.g., NSLog or debugPrint) that includes
the original color description and notes the fallback, then return the current
(0,0,0) fallback; update the comment/docstring for rgbComponents to document
that non-convertible colors fall back to black so callers are aware of the
behavior.
Sources/Providers/StatuspageIOFetcher.swift (1)

20-23: Consider validating the host parameter.

The host parameter is interpolated directly into a URL string. While URL(string:) will return nil for malformed URLs, a malicious or misconfigured host value could potentially redirect requests. If host comes from user input or external configuration, consider validating it against an allowlist of known Statuspage hosts (e.g., status.claude.com, status.openai.com).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/StatuspageIOFetcher.swift` around lines 20 - 23,
StatuspageIOFetcher.fetch currently interpolates the host directly into a URL
which can be manipulated; before building the URL, validate the host parameter
against an allowlist of known Statuspage hosts (e.g., a Set like allowedHosts =
["status.openai.com", "status.claude.com"]), ensure the host contains no
scheme/path/credentials (use URLComponents.host or strict regex to enforce
hostname-only), and throw a clear error (add or reuse a StatuspageIOFetchError
case such as .invalidHost) when validation fails instead of proceeding to
URL(string:). This protects the code from malicious or misconfigured host values
while keeping URL construction and the existing error handling in fetch intact.
Sources/Providers/ProviderAccountStore.swift (2)

288-303: writeAdd delete-then-add on duplicate may race with concurrent operations.

If errSecDuplicateItem occurs, the code deletes the existing item and retries the add (lines 294-299). However, between the delete and retry, a concurrent load or update could fail unexpectedly. Since this is already inside a detached task and the store methods don't serialize concurrent calls to the same account ID, this race window exists. Consider using SecItemUpdate as the fallback instead of delete-then-add, or documenting this as an acceptable race for the duplicate-item edge case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ProviderAccountStore.swift` around lines 288 - 303, The
writeAdd function currently does a delete-then-add on errSecDuplicateItem which
can race with concurrent operations; instead, on receiving errSecDuplicateItem
from SecItemAdd, call SecItemUpdate using the same
matchQuery(service:accountId:) as the query and addAttributes(payload:) as the
attributes-to-update, and if SecItemUpdate returns an error other than
errSecSuccess then throw ProviderAccountStoreError.keychain with that status
(fall back to throwing only if update fails); keep the original SecItemAdd path
otherwise and retain use of SecItemAdd, matchQuery, addAttributes,
SecItemUpdate, and ProviderAccountStoreError.keychain to locate the change.

237-243: probePresence() is synchronous and blocks MainActor during app initialization.

probePresence() performs a synchronous SecItemCopyMatching() call during loadIndex() (line 160), which runs on the MainActor in init(). The codebase design explicitly delegates keychain operations to detached tasks (see comment lines 42–45), but probePresence() is kept synchronous to distinguish errSecItemNotFound from transient failures during orphan account pruning. While this tradeoff is intentional and documented (lines 233–236), it can cause startup hitches on slow keychain responses. Either wrap the pruning logic in an async detached task or add explicit documentation of the MainActor startup impact and why it's necessary.

🤖 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/cmuxApp.swift`:
- Around line 6471-6499: The alert currently displays the raw
providerRemoveError (set in the catch of Task in the removal confirmation) which
bypasses localization; instead map the caught error from
ProviderAccountStore.shared.remove(id:) to a stable localization key (e.g.,
switch on error cases or use an NSError domain/code mapping) and set
providerRemoveError to a localized String(localized:
"providers.accounts.remove.error.<case>", defaultValue: "...") for UI
presentation, while keeping the original error only in the NSLog call; update
the alert message binding to use that localized providerRemoveError and ensure
every user-facing string uses String(localized:..., defaultValue:...).

In `@Sources/Providers/ClaudeProvider.swift`:
- Around line 62-87: The validator and fetcher disagree on how many leading
"sessionKey=" prefixes to remove, allowing inputs like
"sessionKey=sessionKey=abc" to pass validation but fail auth; fix by making
canonicalization deterministic: update strippedSessionKey(_:) so it removes any
repeated leading "sessionKey=" prefixes (e.g., loop dropping the prefix while
trimmed.hasPrefix("sessionKey=")) and return that canonical body, then have
isValidSessionKey(_:) call strippedSessionKey(_:) (as it already does) and
ensure ClaudeUsageFetcher.fetch also uses strippedSessionKey(_:) when
constructing the Cookie header so both validation (isValidSessionKey) and fetch
use the same canonical form.

In `@Sources/Providers/ClaudeUsageFetcher.swift`:
- Around line 28-29: The network error case in ClaudeUsageFetcher currently
interpolates underlying.localizedDescription into the user-facing
String(localized: "claude.usage.error.network", ...), which mixes OS text into
the UI; change the returned text to a fully localized summary only (e.g. use
String(localized: "claude.usage.error.network", defaultValue: "Network error")),
and if you still need the underlying error for debugging or telemetry, log or
send underlying.localizedDescription separately from the UI path (keep this in
the same case .network(underlying) branch and ensure the log/telemetry call is
not shown to users).

In `@Sources/Providers/CodexProvider.swift`:
- Around line 9-16: The isValidAccessToken function currently trims only edges
but allows interior control/whitespace characters; update isValidAccessToken to
reject any token that contains control characters or any whitespace/newline
anywhere in the trimmed string (e.g., check that trimmed.rangeOfCharacter(from:
.whitespacesAndNewlines.union(.controlCharacters)) is nil) before proceeding
with the existing JWT-segmentation logic (the
trimmed.split/segments.count/segments.allSatisfy checks) so tokens with embedded
spaces/newlines are considered invalid.

In `@Sources/Providers/CodexUsageFetcher.swift`:
- Around line 31-32: The .network case in CodexUsageFetcher currently
interpolates underlying.localizedDescription into a user-facing
String(localized: "codex.usage.error.network", defaultValue: "Network error:
\(underlying.localizedDescription)"), which exposes raw OS text; change it to
return a fully localized summary only (e.g., String(localized:
"codex.usage.error.network", defaultValue: "Network error")) and emit the raw
underlying.localizedDescription to a debug/error logger (e.g., logger.error or
os_log) so the UI stays localized while the detailed message is preserved in
logs; update the .network case to use only the localized summary and add a
separate logging call that records underlying.localizedDescription.

In `@Sources/Providers/ProviderAccountStore.swift`:
- Around line 108-121: In remove(id: UUID) (which calls
ProviderAccountKeychain.delete(for:service:) and then saveIndex(current)), catch
errors thrown by saveIndex(current) so you don't re-throw after the keychain
secret was already deleted: update the in-memory accounts = current regardless,
wrap the saveIndex call in a do/catch, and in the catch log the failure (using
your project's logging facility or a fallback) instead of throwing
ProviderAccountStoreError so the credential-free index entry won't surface as a
broken UI item; keep throwing for the initial notFound and keychain delete
failures only.

In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 435-441: The hard-coded .frame(width: 46, alignment: .trailing) on
the Text(metaText) (the countdown column) truncates translated countdown
strings; remove the fixed width and let the text size adapt by replacing
.frame(width: 46, alignment: .trailing) with a flexible alternative such as
.frame(minWidth: 46, alignment: .trailing) or no explicit width at all (or use
.fixedSize() / .layoutPriority(1) if you need to prevent truncation) so
localized strings can expand beyond the English-only 46pt constraint.

---

Duplicate comments:
In `@Sources/Sidebar/ProviderAccountsFooterPanel.swift`:
- Around line 446-463: The tooltip currently concatenates localized fragments
(percentText, resetsPrefix) with Swift string interpolation in tooltipText,
preventing translators from reordering phrases; change this to use fully
localized format strings instead: replace the composed lines in tooltipText with
calls to String(localized: ...) that accept positional placeholders (e.g. a key
like "providers.accounts.usage.tooltip" with placeholders for label, percent,
and optional reset time, and another key for the "not started" variant), pass
label, percentText (or resetTime from formatResetTooltip) into those localized
format strings, and remove inline concatenation; ensure formatResetTooltip still
returns a localized time string but does not bake in surrounding words so the
final localization keys control full sentence order.

---

Nitpick comments:
In `@Sources/Providers/ProviderAccountStore.swift`:
- Around line 288-303: The writeAdd function currently does a delete-then-add on
errSecDuplicateItem which can race with concurrent operations; instead, on
receiving errSecDuplicateItem from SecItemAdd, call SecItemUpdate using the same
matchQuery(service:accountId:) as the query and addAttributes(payload:) as the
attributes-to-update, and if SecItemUpdate returns an error other than
errSecSuccess then throw ProviderAccountStoreError.keychain with that status
(fall back to throwing only if update fails); keep the original SecItemAdd path
otherwise and retain use of SecItemAdd, matchQuery, addAttributes,
SecItemUpdate, and ProviderAccountStoreError.keychain to locate the change.

In `@Sources/Providers/ProviderUsageColorSettings.swift`:
- Around line 188-197: The rgbComponents getter silently returns black when
NSColor(self).usingColorSpace(.sRGB) is nil; change it to log a warning in debug
builds and preserve current behavior in release: wrap the nil branch in a `#if`
DEBUG block and emit a clear warning (e.g., NSLog or debugPrint) that includes
the original color description and notes the fallback, then return the current
(0,0,0) fallback; update the comment/docstring for rgbComponents to document
that non-convertible colors fall back to black so callers are aware of the
behavior.

In `@Sources/Providers/StatuspageIOFetcher.swift`:
- Around line 20-23: StatuspageIOFetcher.fetch currently interpolates the host
directly into a URL which can be manipulated; before building the URL, validate
the host parameter against an allowlist of known Statuspage hosts (e.g., a Set
like allowedHosts = ["status.openai.com", "status.claude.com"]), ensure the host
contains no scheme/path/credentials (use URLComponents.host or strict regex to
enforce hostname-only), and throw a clear error (add or reuse a
StatuspageIOFetchError case such as .invalidHost) when validation fails instead
of proceeding to URL(string:). This protects the code from malicious or
misconfigured host values while keeping URL construction and the existing error
handling in fetch intact.
🪄 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: e2c222f5-4023-4340-864b-65de68389034

📥 Commits

Reviewing files that changed from the base of the PR and between ddaef05 and edb8f75.

⛔ Files ignored due to path filters (11)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-degraded.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (7)
  • README.md
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/Providers/ProviderISO8601DateParser.swift
  • docs/usage-monitoring-setup.md
  • docs/providers.md
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/UsageProvider.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/ContentView.swift
  • cmuxTests/ProviderTests.swift

Comment thread Sources/cmuxApp.swift
Comment on lines +62 to +87
static func isValidSessionKey(_ sessionKey: String) -> Bool {
let body = strippedSessionKey(sessionKey)
guard !body.isEmpty else { return false }
var disallowed = CharacterSet.controlCharacters
// `;` and `,` are the separators that would split the `Cookie`
// header and let a paste smuggle extra directives. `=` is left in
// because cookie values (e.g. base64-padded tokens) legitimately
// contain it.
disallowed.insert(charactersIn: ";,\n\r")
// Embedded whitespace in a cookie value is not a valid sessionKey and
// the server would reject it outright; catch it here so the editor
// shows the right guidance instead of surfacing a fetch failure.
disallowed.formUnion(.whitespacesAndNewlines)
return body.rangeOfCharacter(from: disallowed) == nil
}

/// Trims whitespace and removes a single leading `sessionKey=` prefix.
/// Shared with the fetch path so the editor and the outgoing `Cookie`
/// header always evaluate the same canonical string.
static func strippedSessionKey(_ sessionKey: String) -> String {
let trimmed = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let prefix = "sessionKey="
if trimmed.hasPrefix(prefix) {
return String(trimmed.dropFirst(prefix.count))
}
return trimmed
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make validation and fetch-time canonicalization agree.

isValidSessionKey(_:) strips a leading sessionKey= again, but ClaudeUsageFetcher.fetch only strips once before sending the Cookie header. That means sessionKey=sessionKey=abc validates here and is later sent as sessionKey=sessionKey=abc, which will fail auth. Reject repeated prefixes or canonicalize to one stable body before both validation and fetch.

Possible fix
     static func isValidSessionKey(_ sessionKey: String) -> Bool {
         let body = strippedSessionKey(sessionKey)
         guard !body.isEmpty else { return false }
+        guard !body.hasPrefix("sessionKey=") else { return false }
         var disallowed = CharacterSet.controlCharacters
         disallowed.insert(charactersIn: ";,\n\r")
         disallowed.formUnion(.whitespacesAndNewlines)
         return body.rangeOfCharacter(from: disallowed) == 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 62 - 87, The validator
and fetcher disagree on how many leading "sessionKey=" prefixes to remove,
allowing inputs like "sessionKey=sessionKey=abc" to pass validation but fail
auth; fix by making canonicalization deterministic: update
strippedSessionKey(_:) so it removes any repeated leading "sessionKey=" prefixes
(e.g., loop dropping the prefix while trimmed.hasPrefix("sessionKey=")) and
return that canonical body, then have isValidSessionKey(_:) call
strippedSessionKey(_:) (as it already does) and ensure ClaudeUsageFetcher.fetch
also uses strippedSessionKey(_:) when constructing the Cookie header so both
validation (isValidSessionKey) and fetch use the same canonical form.

Comment thread Sources/Providers/ClaudeUsageFetcher.swift Outdated
Comment thread Sources/Providers/CodexProvider.swift
Comment thread Sources/Providers/CodexUsageFetcher.swift Outdated
Comment thread Sources/Providers/ProviderAccountStore.swift
Comment thread Sources/Sidebar/ProviderAccountsFooterPanel.swift Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue 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="Sources/Providers/ProviderAccountStore.swift">

<violation number="1" location="Sources/Providers/ProviderAccountStore.swift:160">
P2: Synchronous keychain presence checks run on `@MainActor` during index load, which can block UI during startup/reload.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// keychain keep the entry so a later launch still sees it.
let pruned = decoded.filter { account in
let service = serviceName(for: account)
let status = ProviderAccountKeychain.probePresence(for: account.id, service: service)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 15, 2026

Choose a reason for hiding this comment

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

P2: Synchronous keychain presence checks run on @MainActor during index load, which can block UI during startup/reload.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Providers/ProviderAccountStore.swift, line 160:

<comment>Synchronous keychain presence checks run on `@MainActor` during index load, which can block UI during startup/reload.</comment>

<file context>
@@ -0,0 +1,339 @@
+        // keychain keep the entry so a later launch still sees it.
+        let pruned = decoded.filter { account in
+            let service = serviceName(for: account)
+            let status = ProviderAccountKeychain.probePresence(for: account.id, service: service)
+            if status == errSecItemNotFound {
+                NSLog("ProviderAccountStore: dropping orphan account \(account.id) (keychain \(service) missing secret)")
</file context>
Fix with Cubic

@tranquillum tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch from edb8f75 to 6a6d9c0 Compare April 15, 2026 21:02
@tranquillum
Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 15, 2026

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@tranquillum I have started the AI code review. It will take a few minutes to complete.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

The PR description images show:

  • usage-monitoring-overview: Sidebar footer expanded — CLAUDE (Personal: Sess 12%/Week 10%; Work: Sess 100%/Week 14%) and CODEX (Personal: Sess 27%/Week 8%), each with green "Operational" status dots.
  • usage-monitoring-popover-claude/codex: Per-account Sess/Week bars, exact reset countdowns, provider status, and "Refresh now" / "Manage accounts…" actions.
  • usage-monitoring-settings-filled: Settings → AI Usage Monitoring with CLAUDE/CODEX account rows (Edit/Remove), color pickers, and threshold steppers (Low→Mid: 85, Mid→High: 95) with a gradient preview.
  • usage-monitoring-editor-claude/codex: Account editor sheets with Display name + credential fields.
  • usage-monitoring-sidebar-collapsed: Compact > CLAUDE • Operational and > CODEX • Operational rows.

@tranquillum: Kicking off another review pass! Here is the current consolidated state of findings — I've updated the status on two items that are already handled in the code:


🔴 High (open):

  1. Runtime-interpolated String(localized:) keys (ProviderAccountsPopover.swift) — Text(String(localized: "Updated \(formattedTime(snapshot.fetchedAt))")) uses a runtime-varying string as the catalog lookup key, defeating localization. Fix by using a stable key with defaultValue:

    // Before
    Text(String(localized: "Updated \(formattedTime(snapshot.fetchedAt))"))
    // After
    Text(String(localized: "providers.accounts.popover.updated",
                defaultValue: "Updated \(formattedTime(snapshot.fetchedAt))"))
  2. Synchronous Keychain SecItem* on the main thread (ProviderAccountEditorSheet.swift / ProviderAccountStore.swift) — onAppear calls secret(for:) synchronously on @MainActor, which blocks the UI 100–500 ms under Keychain contention. Move Keychain I/O to Task.detached and marshal UI updates back to @MainActor.


🟡 Medium (open):

  1. Cookie header semicolon injection — ✅ Already addressed: ProviderHTTP.sanitizeHeaderValue strips ; and , at the HTTP layer before headers are applied.

  2. Stale ghost account on remove() split failure (ProviderAccountStore.swift) — if SecItemDelete succeeds but saveIndex fails, the stale index entry reappears on next launch as a secret-less account. Mitigation: prune orphan index entries in loadIndex() by probing Keychain presence for each entry.

  3. Timer ticks silently dropped (ProviderAccountsController.swift) — non-forced 60s timer ticks are discarded when currentTask != nil, potentially delaying the next refresh by up to ~2 min under slow networks. Recommend a coalescing/pending-flag pattern instead of a silent drop.


🔵 Low / Nit (open):

  1. ProviderUsageColorSettings tests write to UserDefaults.standard — inject a per-test UserDefaults suite for isolation.

  2. ProviderSection.emptyPlaceholder branch is unreachable in normal flow (providers always have accounts when sections are instantiated) — add a clarifying comment.

  3. Missing ProviderSecret redaction tests — ✅ Already addressed: ProviderSecretRedactionTests in cmuxTests/ProviderTests.swift covers description, debugDescription, Swift.dump, and string interpolation.

  4. CFBoolean === identity check in CodexUsageFetcher.swiftCFGetTypeID(number) == CFBooleanGetTypeID() is more explicit and idiomatic.


The two highest-priority items remain #1 (localization keys) and #2 (Keychain I/O on the main thread).


🧠 Learnings used
Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-02T21:40:59.098Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session snapshot logic, detected foreground command lines are only recorded if they pass SessionRestoreCommandSettings.isCommandAllowed(...). Commit f887189 moved gating to the SessionForegroundProcessCache so unallowed (potentially sensitive) commands are never written to the session JSON.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-05T21:26:10.710Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, applySessionPanelMetadata() must gate listeningPorts restoration on the per-panel snapshot.terminal?.isRemoteBacked flag (not workspace-wide remoteTerminalStartupCommand()), so that local panels are always eligible for port restore regardless of current SSH state. Fixed in commit 4d0fd871 (PR `#2545`).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2884
File: Sources/AppIconDockTilePlugin.swift:16-19
Timestamp: 2026-04-14T08:50:27.729Z
Learning: Repo: manaflow-ai/cmux — Sources/AppIconDockTilePlugin.swift — Two-layer icon persistence contract (PR `#2884`):
- App process (Sources/cmuxApp.swift): AppIconMode.automatic returns nil for imageName to avoid writing custom icons to the bundle from the app side (runtime-only swaps).
- Dock plugin (Sources/AppIconDockTilePlugin.swift): DockTileAppIconMode.automatic intentionally returns a concrete NSImage.Name (AppIconDark or AppIconLight) based on the current effective appearance, so the plugin persists an appearance-matched icon to the bundle. This ensures the Dock shows the correct icon after force-kill/quit instead of falling back to the static bundle asset.
- Do NOT flag DockTileAppIconMode.automatic returning a concrete image name as a bug; it is the intentional Dock-side persistence path. Only when the user explicitly selects light or dark mode is the plugin's behavior identical to cmuxApp.swift. The .automatic branch must NOT return nil from the plugin.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2662
File: Sources/TabManager.swift:1994-1995
Timestamp: 2026-04-07T05:27:49.028Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In applyWorkspacePullRequestRefreshResults(...), resolution .unsupportedRepository must (1) clear any existing workspace.panelPullRequests[panelId] entry and (2) reset lastTerminalState timestamp. scheduleNextWorkspacePullRequestPoll(...) sets nextPollAt to a jittered background interval for .unsupportedRepository, avoiding hot-looping `git remote -v`. Intent: when a panel moves to a non-GitHub or otherwise unsupported repo, clear the sidebar PR badge and back off polling.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:4965-4988
Timestamp: 2026-04-01T09:51:45.737Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, WorkspaceRemoteSessionController.updateRemotePortScanTTYsLocked(_:) must drop remoteScannedPortsByPanel entries when a panel’s TTY changes (compare previousTTYNames vs nextTTYNames). It should also use keepPolledRemotePortsUntilTTYScan to retain the last host-wide polling snapshot only until the first TTY-scoped scan completes, and clear it when no TTYs are tracked.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:679-709
Timestamp: 2026-04-05T20:54:35.429Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift session restore, SessionTerminalPanelSnapshot now includes isRemoteBacked. createPanel(from:inPane:) uses this per-panel flag (panelWasRemoteBacked) to: (1) pass initialInput only for local panels, and (2) persist panelRestoreCommands only for local panels. Remote-backed terminals also omit restore/detected commands and have listeningPorts [] in the snapshot.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: Sources/Workspace.swift:8178-8197
Timestamp: 2026-04-01T22:46:08.900Z
Learning: Repo: manaflow-ai/cmux — Do not persist remote-detected listening ports. In Sources/Workspace.swift, sessionPanelSnapshot must emit [] for listeningPorts when a panel is remote (panelId ∈ remoteDetectedSurfaceIds or isRemoteTerminalSurface), and applySessionPanelMetadata must not restore remote ports. Remote detections remain in-memory only; covered by SessionPersistenceTests.testSessionSnapshotSkipsTransientRemoteListeningPorts.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: CLI/cmux.swift:462-499
Timestamp: 2026-03-09T02:08:54.956Z
Learning: Repo: manaflow-ai/cmux
PR: `#1089`
File: CLI/cmux.swift
Component: ClaudeHookTagExtractor.extractTags(subtitle:body:)
Learning: For Claude Code session tag extraction, pre-redact sensitive spans (UUIDs, emails, access tokens, filesystem paths, ENV_VAR=..., long numerics) across the combined body+subtitle using unanchored sensitiveSpanPatterns before tokenization. Then tokenize and still filter each token with anchored sensitivePatterns. Rationale: prevents PII/path fragments from slipping into searchable tags after delimiter splitting.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2528
File: Sources/AppDelegate.swift:2196-2200
Timestamp: 2026-04-03T03:36:45.112Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate.swift, when KeyboardShortcutSettings.didChangeNotification fires, AppDelegate must clear configured-chord caches (pendingConfiguredShortcutChord and activeConfiguredShortcutChordPrefixForCurrentEvent) via clearConfiguredShortcutChordState() before refreshing tooltips/UI. Also clear chord state on applicationWillResignActive to avoid cross-activity leakage. Verified by cmuxTests/AppDelegateShortcutRoutingTests.swift::testShortcutChangeClearsPendingConfiguredChord.

Learnt from: lucasward
Repo: manaflow-ai/cmux PR: 1903
File: Sources/ContentView.swift:13069-13113
Timestamp: 2026-03-21T06:23:38.764Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, DraggableFolderNSView.updateIcon() intentionally applies the "sidebarMonochromeIcons" setting only on view creation (no live observer). Do not add defaults observers/AppStorage for live refresh in feature-scoped PRs; a live-refresh can be considered in a separate follow-up.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/CmuxConfigExecutor.swift:107-118
Timestamp: 2026-04-14T19:56:42.696Z
Learning: Repo: manaflow-ai/cmux — In Sources/CmuxConfigExecutor.swift, executeWorkspaceCommand for wsDef.target == .current intentionally uses closePanel(panelId, force: true) before calling applyCustomLayout. This is by design: soft-closing (force: false) leaves detached panes that applyCustomLayout cannot reconcile. If a non-destructive autoApply variant is needed in the future, it should be opted into explicitly via a separate flag rather than changing the default force: true behavior.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: Sources/Workspace.swift:1072-1075
Timestamp: 2026-04-04T02:21:20.453Z
Learning: Repo: manaflow-ai/cmux — Foreground-auth deferral: In CLI/cmux.swift, PermitLocalCommand and LocalCommand are injected only into the foreground startup SSH command (e.g., via deferredRemoteReconnectLocalCommand(...)) and are not added to the ssh_options payload sent in workspace.remote.configure. Consequently, WorkspaceRemoteSSHBatchCommandBuilder.batchArguments(...) and WorkspaceRemoteSessionController.backgroundSSHOptions(_:) do not need to filter LocalCommand/PermitLocalCommand for batch operations.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-01T09:50:23.728Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift within CMUXCLI.buildInteractiveRemoteShellScript(...), never export CMUX_TAB_ID from the workspace UUID. CMUX_TAB_ID must be surface-scoped: only set it when a surface ID is available (map CMUX_TAB_ID to CMUX_SURFACE_ID). Rationale: tab-action/rename-tab resolve CMUX_TAB_ID before CMUX_SURFACE_ID; workspace-scoped values misroute or fail.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/Workspace.swift:0-0
Timestamp: 2026-04-06T18:23:54.205Z
Learning: Repo: manaflow-ai/cmux — Sources/Workspace.swift: When promoting an existing local terminal to remote-backed in Workspace.seedInitialRemoteTerminalSessionIfNeeded(_), clear both surfaceTTYNames[panelId] and surfaceListeningPorts[panelId] before calling trackRemoteTerminalSurface(_:) to avoid stale local TTY/port metadata in the workspace UI.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, `claudeTeamsLaunchArguments` must use two independent branches: (1) prepend `--teammate-mode auto` only when the caller has NOT already supplied `--teammate-mode`; (2) append `--settings <claudeHooksJSON>` unconditionally (modulo `CMUX_CLAUDE_HOOKS_DISABLED=1` opt-out). These two branches must never be coupled in a single early-return or conditional block — coupling them was the original `#2229` regression (hook JSON silently bypassed when `--teammate-mode auto|manual` was explicit). Mirrors `Resources/bin/claude:208`: `exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"` where `$@` content never gates hook injection.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9691-9711
Timestamp: 2026-04-01T22:58:26.254Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (runClaudeTeams), custom Claude path resolution now trims whitespace and rejects paths that point to the cmux wrapper using isCmuxClaudeWrapper(), before falling back to PATH/bundled. The Resources/bin/claude wrapper also resolves the real path and compares against itself, requiring -f/-x to avoid recursion/self-reference.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9698-9706
Timestamp: 2026-04-01T23:08:19.526Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (CMUXCLI.runClaudeTeams), custom Claude path candidates (CMUX_CUSTOM_CLAUDE_PATH and UserDefaults key "claudeCodeCustomClaudePath") now reject directory-valued paths by checking isDirectory == false before isExecutableFile, aligning with the Resources/bin/claude wrapper’s [[ -f && -x ]] validation and ensuring fallback to PATH/bundled.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2492
File: CLI/cmux.swift:319-321
Timestamp: 2026-04-06T09:45:32.018Z
Learning: Repo: manaflow-ai/cmux — Claude hook stop classification precedence: in CLI/cmux.swift, summarizeClaudeHookStop should detect failures by checking (1) hook_event_name == "StopFailure" or (2) presence of the "error" field; only then fall back to stop_reason != "end_turn" for back-compat. The notification subtitle/body should use String(localized:) with keys claude.hook.stop.verb.stopped/completed and claude.hook.stop.state.stopped/completed.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift `runClaudeTeams`, `setenv("CMUX_CLAUDE_PID", ...)` is set unconditionally before the passthrough-subcommand check, mirroring `Resources/bin/claude:188` which exports `CMUX_CLAUDE_PID=$$` before the subcommand `case` statement. This is intentional design: the env var is dead for `mcp`/`config`/`api-key`/`rc`/`remote-control` paths but harmless, and matching the bash wrapper unconditional export exactly is a deliberate design principle for the claude-teams hook injection feature.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: Sources/GhosttyTerminalView.swift:3759-3761
Timestamp: 2026-04-01T22:57:41.165Z
Learning: Repo: manaflow-ai/cmux — In Sources/cmuxApp.swift, ClaudeCodeIntegrationSettings.customClaudePath(defaults:) trims surrounding whitespace and returns nil for empty/whitespace-only values; callers (e.g., TerminalSurface.createSurface(for:)) can safely set CMUX_CUSTOM_CLAUDE_PATH without additional trimming.

Learnt from: Horacehxw
Repo: manaflow-ai/cmux PR: 2043
File: Resources/bin/claude:0-0
Timestamp: 2026-04-03T07:49:14.139Z
Learning: Repo: manaflow-ai/cmux — In Resources/bin/claude, SELF_DIR is validated with a broad regex `[\"\\' $\`!;|&<>()*?\[\]]` before interpolating it into HOOKS_JSON command strings. Paths that match (unsafe chars including spaces and shell metacharacters) cause the wrapper to fall back to BASE_HOOKS_JSON and emit a warning. This guard-and-fallback is the intentional design; do not suggest adding shell quoting inside the JSON command strings — the guard approach is preferred.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:4036-4038
Timestamp: 2026-04-01T09:50:41.471Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (buildInteractiveRemoteShellScript), remote shells intentionally export CMUX_SOCKET_PATH and CMUX_SOCKET as "127.0.0.1:<relayPort>" and prepend PATH with "$HOME/.cmux/bin"; CMUX_BUNDLED_CLI_PATH points to that wrapper. WorkspaceRemoteSessionController.remoteCLIWrapperScript() detects host:port, reads "~/.cmux/relay/<port>.daemon_path", and delegates to cmuxd-remote. Do not flag the non-UNIX value of CMUX_SOCKET_PATH/CMUX_SOCKET as a bug; it is the relay contract for remote sessions.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/ContentView.swift:6055-6057
Timestamp: 2026-03-09T02:09:08.742Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), within CommandPaletteSwitcherSearchIndexer.metadataKeywordsForSearch(_:detail:), when tags are present, only add "tag" and "topic" to contextKeywords; do not append "claude" unconditionally to avoid false-positive switcher matches.

Learnt from: qkrwpdlr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-23T07:01:33.134Z
Learning: Repo: manaflow-ai/cmux — In Sources/Panels/BrowserPanel.swift, BrowserPickerMessageHandler gates WKScriptMessage delivery via NSEvent.addLocalMonitorForEvents(.leftMouseDown): on Option+click it stamps ProcessInfo.processInfo.systemUptime into lastNativeOptionClickTime; userContentController(_:didReceive:) rejects postMessages where elapsed >= 1.0s and zeroes the timestamp after acceptance (one-shot). The NSEvent monitor is removed in BrowserPickerMessageHandler.deinit. BrowserPanel.bindWebView(_:) removes the old script message handler and nils pickerMessageHandler before re-registering (prevents monitor leaks on profile change/webView replacement). BrowserPanel.deinit also removes the "cmuxPointer" script message handler.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-3021
Timestamp: 2026-04-14T20:00:16.490Z
Learning: Repo: manaflow-ai/cmux — Accessibility convention: For toolbar actions in macOS 26+ SwiftUI .toolbar (Sources/ContentView.swift), use localized .accessibilityLabel (and identifiers) for VoiceOver; AppKit NSToolbarItems in Sources/WindowToolbarController.swift set localized label/toolTip/accessibilityDescription. Label(...).labelStyle(.iconOnly) is not required here as VO picks up the localized accessibilityLabel.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2647
File: Sources/ContentView.swift:2918-2932
Timestamp: 2026-04-14T20:05:27.511Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift (PR `#2647`): On macOS 26, NavigationSplitView provides a native collapse chevron in the sidebar column header when the sidebar is visible. The code intentionally: (1) shows a custom toolbar toggle only when the sidebar is hidden to reopen it, and (2) uses SystemSidebarToggleStripper to remove the system-injected toolbar toggle to prevent duplication. Do not flag “no collapse affordance” while the sidebar is open.
✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 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="Sources/Providers/ClaudeUsageFetcher.swift">

<violation number="1" location="Sources/Providers/ClaudeUsageFetcher.swift:128">
P2: Utilization parsing truncates fractional numeric values instead of rejecting them, which can silently underreport usage and mask API schema changes.</violation>
</file>

<file name="cmuxTests/ProviderTests.swift">

<violation number="1" location="cmuxTests/ProviderTests.swift:466">
P2: Round-trip remove test does not actually verify keychain deletion; it only checks an index-gated `.notFound` path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID() {
return nil
}
return min(max(number.intValue, 0), 100)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 15, 2026

Choose a reason for hiding this comment

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

P2: Utilization parsing truncates fractional numeric values instead of rejecting them, which can silently underreport usage and mask API schema changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Providers/ClaudeUsageFetcher.swift, line 128:

<comment>Utilization parsing truncates fractional numeric values instead of rejecting them, which can silently underreport usage and mask API schema changes.</comment>

<file context>
@@ -0,0 +1,131 @@
+        if CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID() {
+            return nil
+        }
+        return min(max(number.intValue, 0), 100)
+    }
+}
</file context>
Fix with Cubic

try await store.remove(id: account.id)
XCTAssertTrue(store.accounts.isEmpty)
do {
_ = try await store.secret(for: account.id)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 15, 2026

Choose a reason for hiding this comment

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

P2: Round-trip remove test does not actually verify keychain deletion; it only checks an index-gated .notFound path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxTests/ProviderTests.swift, line 466:

<comment>Round-trip remove test does not actually verify keychain deletion; it only checks an index-gated `.notFound` path.</comment>

<file context>
@@ -0,0 +1,500 @@
+        try await store.remove(id: account.id)
+        XCTAssertTrue(store.accounts.isEmpty)
+        do {
+            _ = try await store.secret(for: account.id)
+            XCTFail("Expected .notFound after remove")
+        } catch ProviderAccountStoreError.notFound {
</file context>
Fix with Cubic

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a6d9c05a9

ℹ️ 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".

extension Providers {
static let claude: UsageProvider = UsageProvider(
id: "claude",
displayName: "Claude",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize provider display names

The /workspace/cmux/AGENTS.md policy says all user-facing strings must use String(localized:defaultValue:), but this hardcoded provider label is surfaced directly in the sidebar/status/settings UI and bypasses Localizable.xcstrings. The same issue is repeated for Codex in Sources/Providers/CodexProvider.swift (displayName: "Codex"), so localized builds cannot manage these labels through the translation pipeline.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 486-490: The ProviderAccountStore currently starts
pruneOrphanAccountsIfNeeded() during init and the test sets
keychainServiceResolver only after construction, causing a race; modify
ProviderAccountStore.init(userDefaults:indexKey:) to accept an initial
keychainServiceResolver parameter (or a flag to disable pruning) and assign that
resolver to the instance before any pruning runs, ensuring
keychainServiceResolver is injected at construction time (reference
ProviderAccountStore.init(userDefaults:indexKey:), keychainServiceResolver, and
pruneOrphanAccountsIfNeeded()) so tests never start with the wrong service
mapping.

In `@Sources/Providers/ClaudeUsageFetcher.swift`:
- Around line 123-128: The intUtilization(_:) helper currently truncates
fractional NSNumber values via number.intValue; update it to reject
non-integer/fractional numeric payloads instead of silently truncating: in
intUtilization(_ value: Any?) keep the NSNumber and CFBoolean checks
(CFGetTypeID(...) == CFBooleanGetTypeID()), then verify the numeric value is
integral (e.g. compare number.doubleValue to its rounded/integral form or use
truncatingRemainder(dividingBy: 1) == 0); if it's fractional return nil so
decoding fails, otherwise convert to an Int and clamp with min(max(..., 0), 100)
as before.

In `@Sources/Providers/CodexProvider.swift`:
- Around line 9-19: The isValidAccessToken function currently only checks prefix
and dot segments, allowing characters like ';' in JWT parts; restrict each of
the three dot-separated segments (variable segments) to only base64url
characters (A–Z, a–z, 0–9, '-' and '_') and optionally '=' padding if you want
to accept padded tokens. Modify isValidAccessToken (use trimmed, segments) to
build an allowed CharacterSet or regex for base64url (plus '=' if desired) and
return false if any segment contains a scalar outside that set; keep the
existing prefix check and the three non-empty segments requirement.

In `@Sources/Providers/ProviderAccountStore.swift`:
- Around line 355-363: In readLoad(for:service:) map the SecItemCopyMatching
status so that errSecItemNotFound is converted to
ProviderAccountStoreError.notFound instead of a generic keychain error; update
the guard/throw logic in the ProviderAccountStore.readLoad method to check for
status == errSecSuccess, but if status == errSecItemNotFound throw
ProviderAccountStoreError.notFound, otherwise throw
ProviderAccountStoreError.keychain(status) so callers of secret(for:) get the
missing-account flow.

In `@Sources/Sidebar/ProviderAccountsPopover.swift`:
- Around line 335-353: localizedStatus contains unreachable cases ("resolved"
and "postmortem") because StatuspageIOFetcher filters those out—remove those
case branches from the localizedStatus switch in ProviderAccountsPopover to
avoid dead code; localizedImpact is missing the "maintenance" impact—add a case
for "maintenance" that returns String(localized:
"providers.incident.impact.maintenance", defaultValue: "Maintenance"); finally
replace the current default returns in both localizedImpact and localizedStatus
with a localized fallback String(localized: "providers.incident.unknown",
defaultValue: "Unknown") to handle unexpected API values defensibly; refer to
the localizedImpact and localizedStatus properties and StatuspageIOFetcher
behavior when making these edits.
🪄 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: dbeb4899-7329-4243-b7a5-625f1eb0a529

📥 Commits

Reviewing files that changed from the base of the PR and between edb8f75 and 6a6d9c0.

⛔ Files ignored due to path filters (11)
  • docs/assets/usage-monitoring-add-profile.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-editor-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-overview.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-claude.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-codex.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-popover-degraded.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-empty.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-settings-filled.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar-collapsed.png is excluded by !**/*.png
  • docs/assets/usage-monitoring-sidebar.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • README.md
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Providers/ClaudeProvider.swift
  • Sources/Providers/ClaudeUsageFetcher.swift
  • Sources/Providers/CodexProvider.swift
  • Sources/Providers/CodexUsageFetcher.swift
  • Sources/Providers/ProviderAccount.swift
  • Sources/Providers/ProviderAccountStore.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
  • Sources/Providers/StatuspageIOFetcher.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Sidebar/ProviderAccountEditorSheet.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift
  • Sources/Sidebar/ProviderAccountsPopover.swift
  • Sources/Sidebar/ProviderAccountsSettingsSection.swift
  • Sources/cmuxApp.swift
  • cmuxTests/ProviderTests.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md
✅ Files skipped from review due to trivial changes (8)
  • README.md
  • Sources/Providers/ProviderISO8601DateParser.swift
  • Sources/Providers/ProviderRegistry.swift
  • docs/providers.md
  • docs/usage-monitoring-setup.md
  • Sources/Providers/ProviderHTTP.swift
  • Sources/Providers/UsageProvider.swift
  • Sources/Providers/ProviderUsageColorSettings.swift
🚧 Files skipped from review as they are similar to previous changes (6)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/Providers/ClaudeProvider.swift
  • Sources/cmuxApp.swift
  • Sources/ContentView.swift
  • Sources/Providers/ProviderAccountsController.swift
  • Sources/Sidebar/ProviderAccountsFooterPanel.swift

Comment on lines +486 to +490
let reloaded = ProviderAccountStore(
userDefaults: userDefaults,
indexKey: "cmux.tests.providers.accounts.index"
)
reloaded.keychainServiceResolver = { [testService] _ in testService! }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inject the test Keychain service before the store starts pruning.

ProviderAccountStore.init(userDefaults:indexKey:) kicks off pruneOrphanAccountsIfNeeded() immediately. Here reloaded is constructed with the default resolver and only then switched to testService, so the background prune can race first, look in the wrong Keychain service, and delete the row you just created as an orphan. That makes this persistence test flaky. Please make the resolver injectable at initialization time (or otherwise suppress pruning for test stores) so the instance never boots with the wrong service mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/ProviderTests.swift` around lines 486 - 490, The
ProviderAccountStore currently starts pruneOrphanAccountsIfNeeded() during init
and the test sets keychainServiceResolver only after construction, causing a
race; modify ProviderAccountStore.init(userDefaults:indexKey:) to accept an
initial keychainServiceResolver parameter (or a flag to disable pruning) and
assign that resolver to the instance before any pruning runs, ensuring
keychainServiceResolver is injected at construction time (reference
ProviderAccountStore.init(userDefaults:indexKey:), keychainServiceResolver, and
pruneOrphanAccountsIfNeeded()) so tests never start with the wrong service
mapping.

Comment on lines +123 to +128
private static func intUtilization(_ value: Any?) -> Int? {
guard let number = value as? NSNumber else { return nil }
if CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID() {
return nil
}
return min(max(number.intValue, 0), 100)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check file size and content
wc -l Sources/Providers/ClaudeUsageFetcher.swift

Repository: manaflow-ai/cmux

Length of output: 106


🏁 Script executed:

# Read the full file to understand context
cat -n Sources/Providers/ClaudeUsageFetcher.swift

Repository: manaflow-ai/cmux

Length of output: 6979


🏁 Script executed:

# Search for usages of intUtilization to understand how it's called
rg "intUtilization" -B 3 -A 3

Repository: manaflow-ai/cmux

Length of output: 1936


🏁 Script executed:

# Check for error handling patterns in the file
rg "return nil|throw|invalid_params" Sources/Providers/ClaudeUsageFetcher.swift -B 2 -A 1

Repository: manaflow-ai/cmux

Length of output: 2324


Reject fractional utilization payloads instead of truncating them.

The docstring declares this helper accepts an "integer utilization value," yet number.intValue silently truncates fractional values like 12.9 to 12. This masks API schema drift instead of failing fast with .decoding as intended.

Proposed fix
     private static func intUtilization(_ value: Any?) -> Int? {
         guard let number = value as? NSNumber else { return nil }
         if CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID() {
             return nil
         }
-        return min(max(number.intValue, 0), 100)
+        let raw = number.doubleValue
+        guard raw.isFinite, raw.rounded(.towardZero) == raw else { return nil }
+        return min(max(Int(raw), 0), 100)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ClaudeUsageFetcher.swift` around lines 123 - 128, The
intUtilization(_:) helper currently truncates fractional NSNumber values via
number.intValue; update it to reject non-integer/fractional numeric payloads
instead of silently truncating: in intUtilization(_ value: Any?) keep the
NSNumber and CFBoolean checks (CFGetTypeID(...) == CFBooleanGetTypeID()), then
verify the numeric value is integral (e.g. compare number.doubleValue to its
rounded/integral form or use truncatingRemainder(dividingBy: 1) == 0); if it's
fractional return nil so decoding fails, otherwise convert to an Int and clamp
with min(max(..., 0), 100) as before.

Comment on lines +9 to +19
static func isValidAccessToken(_ token: String) -> Bool {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.hasPrefix("eyJ") else { return false }
let illegal = CharacterSet.whitespacesAndNewlines.union(.controlCharacters)
if trimmed.unicodeScalars.contains(where: { illegal.contains($0) }) {
return false
}
// Keep empty subsequences so consecutive or leading/trailing dots
// surface as a bad token ("eyJ..abc.def" must not pass as 3 segments).
let segments = trimmed.split(separator: ".", omittingEmptySubsequences: false)
return segments.count == 3 && segments.allSatisfy { !$0.isEmpty }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restrict access-token segments to JWT/base64url characters.

Right now anything with the eyJ prefix and three non-empty dot-separated segments passes, including tokens like eyJ;bad.payload.sig. That pushes obviously invalid credentials through to the fetch path and turns input mistakes into opaque 401s.

💡 Proposed fix
     static func isValidAccessToken(_ token: String) -> Bool {
         let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
         guard trimmed.hasPrefix("eyJ") else { return false }
         let illegal = CharacterSet.whitespacesAndNewlines.union(.controlCharacters)
         if trimmed.unicodeScalars.contains(where: { illegal.contains($0) }) {
             return false
         }
+        let allowedJWTCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
         // Keep empty subsequences so consecutive or leading/trailing dots
         // surface as a bad token ("eyJ..abc.def" must not pass as 3 segments).
         let segments = trimmed.split(separator: ".", omittingEmptySubsequences: false)
-        return segments.count == 3 && segments.allSatisfy { !$0.isEmpty }
+        return segments.count == 3 && segments.allSatisfy { segment in
+            !segment.isEmpty && segment.unicodeScalars.allSatisfy { allowedJWTCharacters.contains($0) }
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/CodexProvider.swift` around lines 9 - 19, The
isValidAccessToken function currently only checks prefix and dot segments,
allowing characters like ';' in JWT parts; restrict each of the three
dot-separated segments (variable segments) to only base64url characters (A–Z,
a–z, 0–9, '-' and '_') and optionally '=' padding if you want to accept padded
tokens. Modify isValidAccessToken (use trimmed, segments) to build an allowed
CharacterSet or regex for base64url (plus '=' if desired) and return false if
any segment contains a scalar outside that set; keep the existing prefix check
and the three non-empty segments requirement.

Comment on lines +355 to +363
private static func readLoad(for accountId: UUID, service: String) throws -> ProviderSecret {
var query = matchQuery(service: service, accountId: accountId)
query[kSecReturnData] = true
query[kSecMatchLimit] = kSecMatchLimitOne
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
throw ProviderAccountStoreError.keychain(status)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Treat missing Keychain items as .notFound.

When a stale index row reaches secret(for:) before orphan pruning finishes, this path currently turns errSecItemNotFound into a generic Keychain error. That surfaces a misleading permissions-style message instead of the normal missing-account flow.

💡 Proposed fix
     private static func readLoad(for accountId: UUID, service: String) throws -> ProviderSecret {
         var query = matchQuery(service: service, accountId: accountId)
         query[kSecReturnData] = true
         query[kSecMatchLimit] = kSecMatchLimitOne
         var result: CFTypeRef?
         let status = SecItemCopyMatching(query as CFDictionary, &result)
-        guard status == errSecSuccess, let data = result as? Data else {
+        if status == errSecItemNotFound {
+            throw ProviderAccountStoreError.notFound
+        }
+        guard status == errSecSuccess, let data = result as? Data else {
             throw ProviderAccountStoreError.keychain(status)
         }
         return try decodeSecret(data)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Providers/ProviderAccountStore.swift` around lines 355 - 363, In
readLoad(for:service:) map the SecItemCopyMatching status so that
errSecItemNotFound is converted to ProviderAccountStoreError.notFound instead of
a generic keychain error; update the guard/throw logic in the
ProviderAccountStore.readLoad method to check for status == errSecSuccess, but
if status == errSecItemNotFound throw ProviderAccountStoreError.notFound,
otherwise throw ProviderAccountStoreError.keychain(status) so callers of
secret(for:) get the missing-account flow.

Comment on lines +335 to +353
private var localizedImpact: String {
switch incident.impact {
case "critical": return String(localized: "providers.incident.impact.critical", defaultValue: "Critical")
case "major": return String(localized: "providers.incident.impact.major", defaultValue: "Major")
case "minor": return String(localized: "providers.incident.impact.minor", defaultValue: "Minor")
case "none": return String(localized: "providers.incident.impact.none", defaultValue: "None")
default: return incident.impact.capitalized
}
}

private var localizedStatus: String {
switch incident.status {
case "investigating": return String(localized: "providers.incident.status.investigating", defaultValue: "Investigating")
case "identified": return String(localized: "providers.incident.status.identified", defaultValue: "Identified")
case "monitoring": return String(localized: "providers.incident.status.monitoring", defaultValue: "Monitoring")
case "resolved": return String(localized: "providers.incident.status.resolved", defaultValue: "Resolved")
case "postmortem": return String(localized: "providers.incident.status.postmortem", defaultValue: "Postmortem")
default: return incident.status.capitalized
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What are the documented possible values for Statuspage incident impactandstatus fields?

💡 Result:

The documented possible values for Statuspage incident fields are: Impact: none, minor, major, critical, maintenance Status (for realtime incidents): investigating, identified, monitoring, resolved Status (for scheduled incidents): scheduled, in_progress, verifying, completed Status (for incident updates): investigating, identified, monitoring, resolved, postmortem, scheduled

Citations:


🏁 Script executed:

# Find and examine the full context of ProviderAccountsPopover.swift
find . -name "ProviderAccountsPopover.swift" -type f

Repository: manaflow-ai/cmux

Length of output: 107


🏁 Script executed:

# Once we locate it, check the broader context around lines 335-353
# and identify where 'incident' data comes from
head -400 Sources/Sidebar/ProviderAccountsPopover.swift | tail -100

Repository: manaflow-ai/cmux

Length of output: 4270


🏁 Script executed:

# Search for incident model definition
rg "struct.*[Ii]ncident|class.*[Ii]ncident" --type swift -A 5

Repository: manaflow-ai/cmux

Length of output: 874


🏁 Script executed:

# Check what keys exist in Localizable.xcstrings for incident impact/status
rg "providers\.incident\.(impact|status)" Resources/Localizable.xcstrings

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if Localizable.xcstrings exists and search differently
find . -name "Localizable.xcstrings" -type f

Repository: manaflow-ai/cmux

Length of output: 93


🏁 Script executed:

# Look for any incident-related keys in the xcstrings file
cat Resources/Localizable.xcstrings | rg -i "incident" -A 3 -B 1

Repository: manaflow-ai/cmux

Length of output: 190


🏁 Script executed:

# Also check the ProviderIncident definition more thoroughly
cat Sources/Providers/UsageProvider.swift | grep -A 20 "struct ProviderIncident"

Repository: manaflow-ai/cmux

Length of output: 487


🏁 Script executed:

# Look for where incidents are parsed/decoded from the API
rg "ProviderIncident\|incident\." Sources/ -A 2 -B 2 | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for where ProviderIncident is created/decoded
rg "ProviderIncident\(" Sources/ -B 3 -A 3

Repository: manaflow-ai/cmux

Length of output: 648


🏁 Script executed:

# Look for API parsing, Statuspage API calls
rg "statuspage|incident" Sources/ -i --type swift | head -40

Repository: manaflow-ai/cmux

Length of output: 4553


🏁 Script executed:

# Get the full structure of UsageProvider to understand incident sources
wc -l Sources/Providers/UsageProvider.swift

Repository: manaflow-ai/cmux

Length of output: 100


🏁 Script executed:

# Check around the incident parsing in UsageProvider
sed -n '1,100p' Sources/Providers/UsageProvider.swift

Repository: manaflow-ai/cmux

Length of output: 1931


🏁 Script executed:

# Examine StatuspageIOFetcher to understand what incident data is fetched
cat Sources/Providers/StatuspageIOFetcher.swift

Repository: manaflow-ai/cmux

Length of output: 3269


🏁 Script executed:

# Check what providers are configured and which ones use Statuspage
rg "fetchStatus.*StatuspageIOFetcher\|StatuspageIOFetcher\.fetch" Sources/ -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if there are localization keys for "maintenance" impact or other missing status values
cat Resources/Localizable.xcstrings | grep -E "(maintenance|scheduled|in_progress|verifying|completed)" -B 2 -A 4

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for where the incident statuses and impacts are used/filtered
rg "resolved|postmortem|investigating|identified|monitoring" Sources/ --type swift | grep -i status | head -20

Repository: manaflow-ai/cmux

Length of output: 1499


🏁 Script executed:

# Check if there are actual test cases or examples of incident data
rg "ProviderIncident\(" Sources/ -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 557


🏁 Script executed:

# Verify what the actual Statuspage.io API response looks like by checking for any mock data
find . -name "*.json" -o -name "*.swift" | xargs rg "investigating|identified" 2>/dev/null | head -20

Repository: manaflow-ai/cmux

Length of output: 1544


🏁 Script executed:

# Check the full incident switch statement context to understand impact color mapping
sed -n '345,365p' Sources/Sidebar/ProviderAccountsPopover.swift

Repository: manaflow-ai/cmux

Length of output: 1065


🏁 Script executed:

# Check if there are any recent incidents from Claude or OpenAI to see what values actually occur
rg "impact|status" Sources/Providers/ --type swift | grep -E "claude|openai" -i -A 3 -B 3

Repository: manaflow-ai/cmux

Length of output: 2145


🏁 Script executed:

# Verify impact/status values from the Statuspage API spec for the integrated hosts
# Check if there's any documentation or comments about expected values
rg "critical|major|minor|investigating|identified|monitoring" Sources/ --type swift -B 3 -A 1 | grep -E "(comment|//|status|impact)" | head -30

Repository: manaflow-ai/cmux

Length of output: 3402


🏁 Script executed:

# Double-check: are "resolved" and "postmortem" actually used somewhere after filtering?
rg "resolved|postmortem" Sources/Sidebar/ProviderAccountsPopover.swift -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 595


Remove dead code and add missing impact localization.

The "resolved" and "postmortem" cases in localizedStatus are unreachable—StatuspageIOFetcher explicitly filters out incidents with these statuses before they reach the UI. Remove those cases.

Additionally, the documented Statuspage API includes "maintenance" as a possible impact value, but it is not localized in the localizedImpact switch. Add:

case "maintenance": return String(localized: "providers.incident.impact.maintenance", defaultValue: "Maintenance")

For the default cases: consider replacing with a localized fallback like String(localized: "providers.incident.unknown", defaultValue: "Unknown") to remain defensible if the API introduces new enum values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Sidebar/ProviderAccountsPopover.swift` around lines 335 - 353,
localizedStatus contains unreachable cases ("resolved" and "postmortem") because
StatuspageIOFetcher filters those out—remove those case branches from the
localizedStatus switch in ProviderAccountsPopover to avoid dead code;
localizedImpact is missing the "maintenance" impact—add a case for "maintenance"
that returns String(localized: "providers.incident.impact.maintenance",
defaultValue: "Maintenance"); finally replace the current default returns in
both localizedImpact and localizedStatus with a localized fallback
String(localized: "providers.incident.unknown", defaultValue: "Unknown") to
handle unexpected API values defensibly; refer to the localizedImpact and
localizedStatus properties and StatuspageIOFetcher behavior when making these
edits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant