Skip to content

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

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

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

Conversation

@tranquillum

@tranquillum tranquillum commented Apr 11, 2026 •

Copy link
Copy Markdown

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 that tracks Claude and Codex subscription usage per account with Session (5h) and Week bars, reset times, incident badges, and per‑provider popovers. Background polling starts on launch (skipped in tests) and stops on quit; credentials are stored in macOS Keychain.

  • New Features

    • Sidebar footer per provider/account with Session/Week bars, reset times, incident dots, and popovers; hides with no accounts and keeps status dots when the sidebar is collapsed.
    • Settings → Usage Monitoring to add/edit/remove accounts and customize usage bar colors (low/mid/high thresholds, optional interpolation); integrated into SettingsNavigationTarget and settings search.
    • Generic provider model (UsageProvider, ProviderRegistry) with ephemeral HTTP sessions, ISO8601 parsing, and allowlisted Statuspage.io hosts; ships Claude and Codex providers with usage + status integration. Background controller (ProviderAccountsController) is wired into app startup/shutdown; ProviderAccountStore persists accounts and stores secrets in Keychain.
  • Docs & Tests

    • Docs: docs/providers.md, docs/usage-monitoring-setup.md; README updated with screenshots and localized strings.
    • Tests for validators (Claude session/org, Codex JWT/account), store/registry, HTTP/ISO8601 parsing, color settings, and settings flows; provider startup is skipped under tests.

Written for commit 2e4d602. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Sidebar AI provider usage panel showing per-account Session (5h) and Week (7d) bars, popovers with reset/incident details, add/edit account sheet, Settings management, Keychain-backed credentials, configurable colors/thresholds with live preview, and Claude & Codex provider integrations with background polling.
  • Bug Fixes

    • Improved footer layout, safer startup/shutdown of provider services, better polling cancellation, timeouts and error reporting.
  • Documentation

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

    • Expanded provider-focused unit and UI tests.

@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

@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

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

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/registry, Keychain-backed per-account store and editor UI, HTTP/ISO8601 utilities, Claude/Codex providers and fetchers, a visibility-aware polling controller, sidebar footer/popover UI, Settings integration, color/threshold settings, tests, and documentation.

Changes

Cohort / File(s) Summary
Xcode manifest & README
GhosttyTabs.xcodeproj/project.pbxproj, README.md
Project manifest updated to include many new sources, UI files, and tests; README features table adds the AI provider usage panel entry.
Documentation
docs/providers.md, docs/usage-monitoring-setup.md, docs/assets/usage-monitoring-overview.png
New docs describing the provider integration model, setup guide, troubleshooting, and an overview screenshot.
Core provider models & registry
Sources/Providers/UsageProvider.swift, Sources/Providers/ProviderRegistry.swift, Sources/Providers/ProviderAccount.swift
Introduced UsageProvider types, credential-field metadata, ProviderAccount/ProviderSecret (with redaction), and ProviderRegistry/Providers namespace.
HTTP, date & status fetcher
Sources/Providers/ProviderHTTP.swift, Sources/Providers/ProviderISO8601DateParser.swift, Sources/Providers/StatuspageIOFetcher.swift
Added ProviderHTTP (ephemeral sessions, sanitized headers, typed errors), ISO8601 date parser, and Statuspage.io incident fetcher with host validation and component filtering.
Providers & fetchers
Sources/Providers/ClaudeProvider.swift, Sources/Providers/ClaudeUsageFetcher.swift, Sources/Providers/CodexProvider.swift, Sources/Providers/CodexUsageFetcher.swift
Added Claude and Codex provider definitions, validators, keychain service names, status metadata, and robust async usage/status fetchers with specific error mapping and parsing rules.
Account storage & Keychain
Sources/Providers/ProviderAccountStore.swift
@mainactor ProviderAccountStore: UserDefaults index + Keychain JSON secrets, async add/update/remove/secret APIs, orphan pruning, cancellation-aware SecItem calls, rollback/logging semantics, and test initializer.
Polling & controller
Sources/Providers/ProviderAccountsController.swift
Visibility-aware polling controller (60s ticks) with generation-based concurrency, per-fetch timeouts, coalescing, status cadence, published snapshots/fetchErrors/incidents, and start/stop/refresh lifecycle.
Color & visualization settings
Sources/Providers/ProviderUsageColorSettings.swift
UserDefaults-backed color hexes and thresholds, interpolation option, clamping/validation, sRGB interpolation, hex parsing/format helpers, preview rendering and reset-to-defaults.
Sidebar UI
Sources/Sidebar/ProviderAccountsFooterPanel.swift, Sources/Sidebar/ProviderAccountsPopover.swift, Sources/Sidebar/ProviderAccountEditorSheet.swift
Footer panel with collapsible provider sections and status labels; popover showing incidents/accounts and actions; editor sheet for add/edit wired to store/controller, with validation, error handling, and Keychain integration.
Settings & app wiring
Sources/Sidebar/ProviderAccountsSettingsSection.swift, Sources/cmuxApp.swift, Sources/AppDelegate.swift, Sources/ContentView.swift
Settings integration and state wiring, editor/removal dialogs, SettingsView changes, AppDelegate starts/stops controller (skip under XCTest; stop on terminate), and sidebar footer inclusion.
Tests
cmuxTests/ProviderTests.swift
Comprehensive tests covering validators, ISO8601 parsing, ProviderSecret redaction, color settings, registry, ProviderHTTP, account store round-trips, reset-label logic, and status ranking.
Project & helper sources
Sources/..., various files referenced in Xcode manifest
Added support helpers (SentryHelper, AppleScriptSupport), dock-tile plugin wiring, UI panels, and updated project build-phase entries to include new sources and tests.

Sequence Diagram(s)

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

    User->>Editor: Save account (displayName + credentials)
    Editor->>Store: add(providerId, displayName, secret) (async)
    Store->>Keychain: SecItemAdd (JSON-encoded secret)
    Keychain-->>Store: OSStatus / success
    Store->>Store: Persist index to UserDefaults
    Store-->>Editor: Return success / throw
    Editor->>Controller: refreshNow()
    Controller-->>Editor: Publish refresh state / finish
Loading
sequenceDiagram
    participant Timer
    participant Controller as ProviderAccountsController
    participant Store as ProviderAccountStore
    participant Fetcher as Provider.fetchUsage
    participant HTTP as ProviderHTTP
    participant UI as ProviderAccountsFooterPanel

    Timer->>Controller: Tick()
    Controller->>Store: Read accounts
    Store-->>Controller: [ProviderAccount]
    loop for each account (concurrent)
        Controller->>Store: secret(for: account.id)
        Store-->>Controller: ProviderSecret
        Controller->>Fetcher: fetchUsage(secret) with timeout
        Fetcher->>HTTP: GET provider usage endpoint (headers sanitized)
        HTTP-->>Fetcher: JSON response / error
        Fetcher-->>Controller: ProviderUsageWindows / fetch error
        Controller->>Controller: Update snapshots & fetchErrors
    end
    alt every Nth tick or forced
        Controller->>Fetcher: fetchStatus(host, componentFilter)
        Fetcher->>HTTP: GET /api/v2/incidents.json
        HTTP-->>Fetcher: JSON response
        Fetcher-->>Controller: [ProviderIncident] / error
        Controller->>UI: Publish incidents & status flags
    end
    Controller->>UI: Publish snapshots & fetchErrors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Poem

🐇 I nibble secrets tucked in keys,
I watch five-hour bars and weekly trees,
I color peaks from low to high,
I poll the clouds and pop a pie,
Hooray — I guard your usage streak.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature added: AI provider usage monitoring for Claude and Codex subscriptions in the sidebar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering the changes, rationale, testing approach, and includes screenshots demonstrating the feature.

✏️ 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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 36 files

@greptile-apps

greptile-apps Bot commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an optional AI Usage Monitoring sidebar panel that tracks Claude and Codex subscription usage (5-hour and weekly windows) with Keychain-backed credentials, a 60-second background polling loop, and Statuspage.io incident surfacing. The implementation is thorough: credentials are never logged or written to disk, the ProviderHTTP layer sanitizes header values, validators block header/URL injection before values reach the wire, and the store implements rollback-on-failure for all Keychain/UserDefaults write pairs.

Remaining findings are all P2: a possible CancellationError surfacing from withTimeout's task-group implicit cleanup on the success path, hard-coded Statuspage component names that could silently miss incidents if either provider renames their components, and a fallback NSLog in the error-message mapper that could leak raw error details from any future unregistered provider type.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 maintenance/robustness concerns with no impact on current correctness.

Prior P0 compile errors are resolved. The security design (Keychain isolation, header sanitisation, no credential logging, ephemeral URLSession) is sound. The concurrency model (actor isolation, generation-gated tasks, rollback on write failure) is well-considered. Remaining P2 observations are about future-proofing and a low-probability edge case in withTimeout.

Sources/Providers/ProviderAccountsController.swift — withTimeout task-group cleanup and fallback NSLog; Sources/Providers/CodexProvider.swift — component filter string stability.

Important Files Changed

Filename Overview
Sources/Providers/ProviderAccountsController.swift Core polling controller: generation-gated refresh loop, per-fetch 20s timeout, occlusion-aware firing, and status fetch every 5th tick. Minor questions around withTimeout implicit task-group cleanup and the fallback NSLog path.
Sources/Providers/ProviderAccountStore.swift Keychain-backed account index with rollback-on-failure write ordering, actor isolation, orphan pruning, and careful reentrancy handling across all CRUD operations.
Sources/Providers/ClaudeUsageFetcher.swift Fetches claude.ai internal usage API; validates orgId/sessionKey before URL construction, percent-encodes the org segment, and rejects boolean NSNumbers masquerading as integers in utilization fields.
Sources/Providers/CodexUsageFetcher.swift Fetches ChatGPT internal WHAM usage API; handles optional accountId header, parses reset_after_seconds as relative offset (robust to null/negative), and rejects bool-bridged NSNumbers.
Sources/Providers/CodexProvider.swift Registers Codex provider with JWT validation and status.openai.com fetcher; hard-coded component filter strings may drift silently if OpenAI renames status-page components.
Sources/Providers/StatuspageIOFetcher.swift Fetches Statuspage.io incidents with an allow-list of permitted hosts; correctly surfaces page-wide (empty-component) incidents regardless of the component filter.
Sources/Providers/ProviderHTTP.swift Ephemeral URLSession factory with cookie/cache isolation and a sanitizer that strips control characters and ;, from header values as a last-line defense.
Sources/Sidebar/ProviderAccountEditorSheet.swift Editor sheet with double-save guard (isSaving), async credential load from Keychain, locked Cancel during save, and error reset on all failure paths.
Sources/Providers/ClaudeProvider.swift Claude provider definition with session-key and org-ID validators; rejects path-traversal characters and cookie-injection bytes before they reach the fetcher.
Sources/AppDelegate.swift Wires ProviderAccountStore and ProviderAccountsController startup/shutdown; correctly skips both under XCTest and stops the controller in applicationWillTerminate.
cmuxTests/ProviderTests.swift Unit tests cover validators, store CRUD round-trip via isolated UserDefaults/Keychain, HTTP/ISO8601 parsing, color settings, and registry; uses public secret(for:) API (prior compile-error concerns resolved).

Sequence Diagram

sequenceDiagram
    participant AD as AppDelegate
    participant PAC as ProviderAccountsController
    participant PAS as ProviderAccountStore
    participant PR as ProviderRegistry
    participant CF as ClaudeUsageFetcher
    participant CxF as CodexUsageFetcher
    participant SF as StatuspageIOFetcher
    participant KC as Keychain

    AD->>PAS: shared (warm up index)
    AD->>PAC: start()
    Note over PAC: DispatchTimer fires every 60s

    PAC->>PAS: accounts (snapshot)
    loop Per account (parallel)
        PAC->>PAS: secret(for: id)
        PAS->>KC: SecItemCopyMatching
        KC-->>PAS: ProviderSecret
        PAS-->>PAC: ProviderSecret
        PAC->>PR: provider(id:)
        PR-->>PAC: UsageProvider
        PAC->>CF: fetchUsage(secret) [Claude]
        CF-->>PAC: ProviderUsageWindows
        PAC->>CxF: fetchUsage(secret) [Codex]
        CxF-->>PAC: ProviderUsageWindows
    end

    Note over PAC: Every 5th tick
    loop Per provider (parallel)
        PAC->>SF: fetch(host:componentFilter:)
        SF-->>PAC: [ProviderIncident]
    end

    PAC-->>PAC: publish snapshots / incidents
    Note over PAC: SwiftUI views observe @Published state
Loading

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

Comment thread cmuxTests/ProviderTests.swift Outdated
Comment thread cmuxTests/ProviderTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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

cubic-dev-ai Bot commented Apr 14, 2026

Copy link
Copy Markdown

@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

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsPopover.swift Outdated
Comment thread Sources/Sidebar/ProviderAccountsSettingsSection.swift Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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
Comment thread Sources/Providers/ClaudeUsageFetcher.swift Outdated
@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

cubic-dev-ai Bot commented Apr 14, 2026

Copy link
Copy Markdown

@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

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

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

@tranquillum
tranquillum force-pushed the feature/agents-usage-monitoring-sidebar-panel branch 2 times, most recently from ac98434 to a0f83e4 Compare April 26, 2026 18:08
@socket-security

socket-security Bot commented Apr 26, 2026 •

Copy link
Copy Markdown

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Adds the persistence layer for AI usage credentials:

- AIUsageKeychain: async wrappers around SecItem* with
  kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
  kSecAttrSynchronizable=false, errSecDuplicateItem fallback to
  SecItemUpdate, strict missing-item handling on update, and a
  presence probe used by orphan pruning. All Keychain calls run on
  Task.detached(priority: .userInitiated) and forward cancellation.
- AIUsageAccountStore @mainactor ObservableObject: keychain-first
  add/update with rollback on index persistence failure, remove
  performs Keychain delete first, orphan pruning runs only on
  errSecItemNotFound (locked-Keychain rows survive). UserDefaults
  index key is c11-prefixed (c11.aiusage.accounts.index).
- c11Tests/AIUsageTests.swift introduces the test bag with two
  subclasses for this commit: AIUsageAccountStoreRoundTripTests
  exercises add/secret/update/remove and persistence across two
  store instances; AIUsageSecretRedactionTests verifies description,
  debugDescription, dump, and string interpolation never leak field
  values while Codable round-trip still preserves them.

Wires the new files into the c11 Sources and c11Tests Sources build
phases.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
… poller

Adds the controller and shared networking pieces:

- AIUsageHTTP: ephemeral URLSession factory (no cookie storage, no
  URLCache, reload-ignoring-local cache policy), GET-JSON helper that
  surfaces CancellationError on cancellation, and sanitizeHeaderValue
  that strips control chars plus ; and , so a header value can never
  smuggle in CRLF or cookie attribute separators.
- AIUsageISO8601DateParser.parse(_:) tries fractional seconds first
  then plain internet-date-time.
- AIUsageStatusPagePoller: allowlists status.claude.com and
  status.openai.com, rejects hosts containing / or :, filters out
  resolved/postmortem/completed incidents, and applies the optional
  componentFilter only to incidents that have a non-empty components
  list (page-wide outages always pass through).
- AIUsagePoller @mainactor controller: 60s DispatchSourceTimer on the
  com.stage11.c11.aiusage.timer queue, 20s per-fetch timeout via
  withThrowingTaskGroup, occlusion-aware skip, status fetch every 5th
  tick, tick coalescing with hasPendingTick, taskGeneration so a stale
  completion can't mutate state for a cancelled tick, and a
  localizedFetchErrorMessage that only surfaces errorDescription for
  errors marked AIUsageAppOwnedError so raw OS wording does not leak.
  Tests-only init takes a tickBody and visibilityProvider seam so
  lifecycle tests can drive the controller without a real timer.
- c11Tests/AIUsageTests.swift gains AIUsageISO8601DateParserTests,
  AIUsageHTTPSessionTests, AIUsageStatusPagePollerHostTests, and
  AIUsagePollerLifecycleTests (start idempotency + refreshNow-after-stop).

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Adds the first concrete provider:

- ClaudeAIValidators: orgId rejects empty, .., /, :, ?, #, %, whitespace,
  and control characters; sessionKey rejects empty, ; , and control
  characters; strippedSessionKey peels off one or more leading
  `sessionKey=` prefixes after trimming whitespace.
- Providers.claude: the AIUsageProvider value with both credential
  fields, status section title, helpDoc URL, fetchUsage closure, and
  fetchStatus closure that calls the allowlisted Claude statuspage
  with a component filter (claude.ai, Claude API, Claude Code).
- ClaudeAIUsageFetcher: hits
  https://claude.ai/api/organizations/<percent-encoded orgId>/usage
  with a sanitized Cookie header carrying the stripped session key,
  re-runs both validators on the stored value, parses
  five_hour.utilization and seven_day.utilization (rejects booleans
  and fractional numbers, clamps 0...100), and surfaces a
  ClaudeAIUsageFetchError that conforms to AIUsageAppOwnedError so
  AIUsagePoller can forward its localized messages.
- AIUsageRegistry.all now includes Providers.claude (was [] in
  commit 1; commit 5 will append Providers.codex).
- Tests: ClaudeAIValidatorTests (orgId / sessionKey accept and reject,
  strippedSessionKey behavior) and AIUsageRegistryClaudeTests
  (registry round-trip + unknown-id returns nil).

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Adds the second concrete provider:

- CodexAIValidators: accessToken trims surrounding whitespace, must
  split on `.` into exactly 3 segments, the first segment starts with
  `eyJ` (JWT URL-safe encoding of `{"`), all chars are in the
  base64url + `.` + `=` allowed set; accountId is optional, empty is
  accepted, but the literal `null` sentinel and any whitespace or
  control characters are rejected.
- Providers.codex: AIUsageProvider value with both credential fields,
  status section title, helpDoc URL, fetchUsage closure, and
  fetchStatus closure that calls the allowlisted OpenAI statuspage
  with a Codex-only component filter.
- CodexAIUsageFetcher: hits
  https://chatgpt.com/backend-api/wham/usage with sanitized
  Authorization and chatgpt-account-id headers (the account-id
  header is omitted when accountId is empty), parses
  rate_limit.primary_window and rate_limit.secondary_window, requires
  limit_window_seconds > 0, rejects negative reset_after_seconds,
  and surfaces a CodexAIUsageFetchError that conforms to
  AIUsageAppOwnedError.
- AIUsageRegistry.all now returns [Providers.claude, Providers.codex].
- Tests: CodexAIValidatorTests (jwt shape, trim, accountId empty/null
  paths) and AIUsageRegistryCodexTests (registry contains codex,
  unique provider ids, every UI provider carries credential fields).

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Adds the user-configurable usage-bar color model:

- AIUsageColorSettings @mainactor ObservableObject with low/mid/high
  hex colors, low/mid and mid/high thresholds (1...98 / 2...99,
  enforced ordered), an interpolation toggle, resetToDefaults, and a
  color(for percent:) that produces a Color whether interpolation is
  on (sRGB blend across the configured stops) or off (discrete
  buckets). UserDefaults keys are c11-prefixed
  (c11.aiusage.color.low / .mid / .high / .lowMidThreshold /
  .midHighThreshold / .interpolate).
- Color(usageHex:) initializer that accepts an optional leading `#`
  and a strict 6-hex-digit body, .usageHexString that round-trips
  back to canonical "#RRGGBB", and .rgbComponents that goes through
  NSColor's sRGB color space so interpolation is in the same space
  the picker presents.
- AIUsageColorSettingsTests covers default fallthrough, threshold
  ordering and clamping, reset, color() bounds, and an interpolation-
  off bucket sanity check; AIUsageColorHexTests covers parsing.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…ditor sheet + popover

- Drops `private` from `SettingsSectionHeader`, `SettingsCard`,
  `SettingsCardRow`, and `SettingsCardDivider` in `Sources/c11App.swift`
  so module-internal AIUsage UI files can compose them. No public
  surface changes (these helpers were not exposed before).
- Sources/AIUsage/UI/AIUsageResetLabel.swift: pure ResetLabel reducer
  used by the footer; awaitingRefresh / sessionNotStarted /
  weekResetUnknown / resetsAt(Date).
- Sources/AIUsage/UI/AIUsageStatusRanking.swift: pure status helpers;
  worstImpactSeverity, worstIncident, statusText (localized).
- Sources/AIUsage/UI/AIUsageEditorSheet.swift: SwiftUI sheet driven by
  AIUsageAccountStore.shared; pre-fills existing secret on .task while
  disabling fields so an async load can't clobber early keystrokes;
  trims and saves; refreshNow on success; surfaces
  AIUsageStoreError.errorDescription or LocalizedError messages.
- Sources/AIUsage/UI/AIUsagePopover.swift: status section (loading /
  fetch failed / all-ok / per-incident rows) plus action section (Add,
  Edit selected, Refresh now); status page link opens via
  NSWorkspace.open.
- Sources/AIUsage/UI/AIUsageSettingsSection.swift: account list with
  Edit/Remove rows, single-button or Menu Add row depending on UI
  provider count, orphan provider card with Remove only, color
  picker rows, threshold steppers, interpolation toggle, and reset
  button. Rendered above the Agent Skills section in commit 8.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…e in AppDelegate

- Sources/AIUsage/UI/AIUsageFooterView.swift: per-provider section
  with collapse-toggle (persisted under c11.aiusage.collapsed.<id>),
  account rows showing Session and Week bars colored by
  AIUsageColorSettings, fetch error fallthrough, and a popover
  trigger for AIUsagePopover. Renders EmptyView when no accounts
  exist so users who never set up an account see no visual change.
- Sources/ContentView.swift: SidebarFooter now wraps both branches of
  its #if DEBUG / #else in a VStack that places AIUsageFooterView
  above SidebarDevFooter / SidebarFooterButtons, matching the plan.
- Sources/AppDelegate.swift: starts AIUsagePoller and warms
  AIUsageAccountStore.shared in applicationDidFinishLaunching, gated
  by isRunningUnderXCTest so test runs do not initiate polling;
  applicationWillTerminate calls AIUsagePoller.shared.stop().
- Sources/c11App.swift: SettingsNavigationTarget gains .aiUsage
  (mapped to .agentsAutomation), SettingsView gains the @StateObject
  references and editor/remove state, agentsAutomationSettingsPage
  renders AIUsageSettingsSection above the existing Agent Skills
  section, the SettingsView body root carries the editor sheet,
  remove confirmationDialog, and remove error alert, and
  resetAllSettings calls AIUsageColorSettings.shared.resetToDefaults
  so global Reset restores the bar palette (account credentials are
  intentionally NOT wiped by reset).

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
- docs/ai-usage-monitoring.md: setup guide for the AI Usage Monitoring
  panel. Covers where it appears, the privacy guarantees (Keychain
  only, ephemeral URLSession, no credential logging, allowlisted
  status hosts), per-provider setup steps for Claude (sessionKey +
  orgId from claude.ai cookies / network requests) and Codex
  (jq-on-~/.codex/auth.json), multiple accounts per provider, and
  troubleshooting for 401, 404, status loading, and occlusion-aware
  polling.
- docs/privacy-endpoints.md: project-wide outbound HTTP catalog. Adds
  the four AI Usage Monitoring endpoints (claude.ai usage,
  chatgpt.com WHAM, status.claude.com, status.openai.com) and the
  hard guarantees that backstop them (allowlisted hosts, Keychain
  only, ephemeral URLSession, sanitized headers, no credential
  logging, occlusion gate). Future features that contact new hosts
  must update this file.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…follow

Adds 83 English-only entries to Resources/Localizable.xcstrings for the
AI Usage Monitoring feature. Coverage:

- provider editor labels, placeholders, and help text for both Claude
  (sessionKey, orgId) and Codex (accessToken, accountId).
- store / generic errors (keychain status, decoding, not found,
  unknown provider, fetch failed, timeout, load secret).
- Claude fetcher errors (invalid orgId/sessionKey, http auth, http,
  bad response, decoding, network).
- Codex fetcher errors (invalid accessToken/accountId, http auth,
  http 404, http, bad response, decoding, network).
- editor sheet (add/edit titles, name, cancel, save, help link).
- settings section (title, color section title, low/mid/high color
  rows, threshold row, interpolate toggle, reset button).
- account row (add, add menu label, edit, remove, summary, unknown
  provider message).
- remove flow (confirmation title/action/body/cancel, error title/
  ok/notFound/decoding/keychain/unknown).
- footer (expand/collapse, accessibility label, session/week labels).
- status / popover (section, openPage, loading, fetchFailed, allOk,
  maintenance/minor/major/critical, refreshNow).

Per the c11 convention, only the English `defaultValue` is authored
here; the translator pass for ja, uk, ko, zh-Hans, zh-Hant, ru is a
follow-up. No em-dashes per memory feedback_no_em_dashes.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…onitoring strings

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…nitoring strings

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…sheet identity

B1: Add `import Security` and `import SwiftUI` to c11Tests/AIUsageTests.swift so
Security symbols (kSecClass, SecItemDelete, CFDictionary) and SwiftUI Color
references resolve.

B2: Wire the sidebar popover Add/Edit closures to present AIUsageEditorSheet.
AIUsageFooterView now owns an editorRequest @State and a .sheet(item:) modifier.
onAdd sets a request with no account; onEdit sets the tapped account.

I7: Replace the per-provider sheet binding (aiUsageEditorProvider) with a
composite AIUsageEditorRequest so SwiftUI re-creates the sheet view per edited
account. Editing two accounts on the same provider no longer shares @State
between the two sheets. Updated all call sites (settings page Edit/Add and the
new footer popover wiring) to set/clear aiUsageEditorRequest.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…status timeout, I6 secondary_window, I8 pending force

I1: CodexAIUsageFetcher.parseWindow now mirrors ClaudeAIUsageFetcher.parseUtilization:
malformed used_percent (nil, NSNull, Bool, non-integer) throws .decoding instead
of clamping to 0%. Tests cover nil/NSNull/Bool/string -> .decoding and a valid
integer happy path.

I3: AIUsageAccountStore.pruneOrphanAccountsIfNeeded now snapshots accounts at
start, computes remove ids from the snapshot, and re-reads accounts after the
async probes complete to compute the final keep list. Concurrent add() during
the prune is no longer clobbered. Adds os_log(.info) when prune actually
removes something.

I4: AIUsageAccountStore.load() catches decode errors with os_log(.error)
including the error description; it does NOT delete the underlying UserDefaults
blob so a future migration can still recover. Test verifies a malformed blob
survives a load() call.

I5: AIUsagePoller wraps the status-page fetchStatus() in
runWithTimeout(perFetchTimeout) so a stalled status fetch cannot hold the tick
indefinitely. Timeout maps to statusFetchFailed[provider.id] = true.

I6: CodexAIUsageFetcher.parseWindows treats missing secondary_window as a soft
case and returns AIUsageWindow(utilization: 0, resetsAt: nil, windowSeconds: 604800)
when the key is absent. The footer reset-label logic for weekResetUnknown
already handles this. Test covers a payload with primary_window but no
secondary_window.

I8: AIUsagePoller now tracks pendingForce separately. When in-flight,
scheduleTick(force:) sets pendingForce |= force. After the in-flight task
completes, the follow-up scheduleTick uses pendingForce instead of false,
preserving the original force intent through one re-entrant queue.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
The AIUsageFooterView bar function now renders a small trailing reset countdown
when providerUsageResetLabel resolves to .resetsAt(date), using a shared
RelativeDateTimeFormatter (.abbreviated). Refresh granularity stays at the
60s tick (no per-second timer). Two new localized strings:
aiusage.reset.accessibility and aiusage.reset.resetsIn (English source values
only; translations follow in a separate pass per project policy).

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
…s, remove order, AppStorage, test flake, test assertions)

M1: AIUsageEditorSheet.canSave trims each value before the empty-check and
before passing to validate, matching what save() already does.

M2: AIUsageSettingsSection main ForEach filters out orphan accounts so an
unknown-provider row does not render twice (once in the main card, once in
the orphan card).

M3: AIUsagePopover refresh button uses the injected `poller` instead of
AIUsagePoller.shared, restoring the DI seam for previews/tests.

M4: docs/ai-usage-monitoring.md adds top-level `## Claude` and `## Codex`
headings so the helpDocURL fragments (#claude / #codex) resolve.

M5: AIUsageAccountStore.remove() persists the index first, commits the in-
memory list, then deletes the Keychain item. A failed Keychain delete leaves
the next prune cycle to clean the orphan rather than abandoning the user
with a stale row.

M6: AIUsageFooterView extracts the per-provider section into a private
AIUsageFooterProviderSection subview that uses @AppStorage with a per-id
key, removing the in-body UserDefaults.standard.bool read.

M7: testRefreshNowAfterStopDoesNotRestartPolling and testStartIsIdempotent
replace 50 ms Task.sleep waits with XCTestExpectation fulfilled from inside
tickBody. Subsequent assertions yield the runloop instead of sleeping.

M8: testStartIsIdempotent now asserts after the first tick has fired (so
the second start() is observed against a real baseline). testColorForPercentBoundsAreClamped
asserts that values below 0 / above 100 clamp to the 0% / 100% buckets and
that 0% and 100% do not produce the same color.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
S11: Replaces an em-dash in AIUsageRegistry.swift's `ui` doc comment with
a comma. docs/ai-usage-monitoring.md and docs/privacy-endpoints.md scanned
clean (no em-dashes present).

S2: Adds a code comment at ClaudeAIUsageFetcher.swift's org-id percent-
encoding site noting that urlPathAllowed includes '/' and '.' but
ClaudeAIValidators.isValidOrgId rejects both, so path traversal would
require Keychain write access.

S6: Adds a comment to AIUsageHTTP.sanitizeHeaderValue noting that ';' and
',' are stripped intentionally for today's known providers (claude.ai
session keys, Codex JWTs); a future token type with legitimate separators
must add a provider-specific sanitizer.

S13: Localization audit run across Sources/ AIUsage call sites and
Resources/Localizable.xcstrings. No gaps found in the load-bearing
direction (every code-referenced "aiusage.*" key has a catalog entry).
A handful of catalog-only stale entries exist (aiusage.error.unknownProvider,
aiusage.help.link, aiusage.remove.error.{decoding,keychain,notFound},
aiusage.status.section); leaving for a future cleanup pass.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
The marker protocol that decides whether an error's errorDescription is
safe to surface to the UI is moved from AIUsagePoller's bottom-of-file
private declaration to its own Sources/Errors/C11AppOwnedError.swift.
Single requirement (var isAppOwned: Bool) is unchanged. The new home
makes the seam discoverable for any future user-facing error pipeline,
not just AI usage.

Conformances updated:
- ClaudeAIUsageFetchError, CodexAIUsageFetchError: AIUsageAppOwnedError
  -> C11AppOwnedError
- AIUsageFetchTimeoutError: now conforms to C11AppOwnedError so its
  localized "Request timed out." string flows through the gate (it was
  already routed via a special-case branch; the conformance makes the
  intent explicit).
- AIUsageStoreError: newly conforms to C11AppOwnedError. Its localized
  descriptions were already authored by c11 and surfaced via a
  special-case in localizedFetchErrorMessage; the conformance lets
  future call sites use the protocol uniformly.

AIUsagePoller.localizedFetchErrorMessage now checks for C11AppOwnedError
instead of the AIUsage-specific protocol.

Project file updated to include the new Sources/Errors/ file.

Inspired-by: manaflow-ai/cmux#2827 by @tranquillum

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BenevolentFutures added a commit to Stage-11-Agentics/c11 that referenced this pull request Apr 27, 2026
Inspired-by: manaflow-ai/cmux#2827 by @tranquillum
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants