Skip to content

feat(desktop): add multi-account AI usage meter - #5844

Closed
AlchemistChaos wants to merge 12 commits into
ParadevAI:mainfrom
AlchemistChaos:feat/minimal-ai-usage-meter
Closed

AlchemistChaos wants to merge 12 commits into
ParadevAI:mainfrom
AlchemistChaos:feat/minimal-ai-usage-meter

Conversation

@AlchemistChaos

@AlchemistChaos AlchemistChaos commented Jul 21, 2026 •

Copy link
Copy Markdown

What & why

Related to #5733. This expands the desktop AI usage meter from an active-account-only meter into a multi-account usage surface modeled after the latest Notch Limits AI meter work.

The top-bar indicator still stays compact and network-cold until opened, but the popover now supports:

  • saved Codex profiles with active/inactive account rows
  • importing the current Codex login
  • adding another Codex account through an isolated CODEX_HOME login flow
  • switching Codex accounts from the meter
  • cached inactive-account readings without showing expired/fabricated capacity
  • menu-bar metric selection for Claude 5-hour, Claude weekly, Claude Fable/Feeable-style limits, and Codex weekly
  • Claude model-scoped limits, including Fable/Feeable-style weekly buckets

The account mutations force-refresh the main-process usage cache, so the next renderer read reflects account imports/switches immediately instead of waiting for the five-minute polling cache.

Provider access and privacy

  • Codex: invokes the locally installed codex app-server --stdio and reads account/rateLimits/read. Superset stores copied Codex profile auth files under its app-owned provider-usage directory, uses temp-file + rename for sensitive writes, backs up the previous active ~/.codex/auth.json before switching, and strips inherited CODEX_HOME/CODEX_SQLITE_HOME during live polling so the switcher and reader use the same default Codex home policy.
  • Claude: reuses Superset's existing credential resolver and reads Anthropic usage with defensive parsing for string or numeric reset timestamps plus model-scoped limits[] buckets.
  • Credentials remain in Electron main. Renderer IPC is Zod-validated and only exposes provider/account labels, status, safe profile names, quota windows, percentages, and reset timestamps.
  • Mutation outputs are Zod-validated, profile names are regex-guarded before filesystem access, CLI errors shown to the renderer are generic, and no telemetry is added.

Review follow-up already handled

  • Uses shell PATH resolution for packaged macOS GUI Codex launches.
  • Accepts numeric and string Claude resets_at values.
  • Adds unavailable-state row coverage.
  • Adds adversarial-review fixes for cache invalidation after account mutations, path traversal rejection, switch-during-poll attribution, expired cached windows, output schemas, CODEX_HOME consistency, and saved-account visibility while Codex usage is unavailable.

How I tested it

  • npx bun@1.3.14 test apps/desktop/src/lib/trpc/routers/provider-usage.test.ts apps/desktop/src/main/lib/provider-usage/index.test.ts apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx — 43 passing
  • npx tsc --noEmit -p apps/desktop/tsconfig.json
  • npx biome check apps/desktop/src/lib/trpc/routers/provider-usage.ts apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts apps/desktop/src/lib/trpc/routers/provider-usage.test.ts apps/desktop/src/main/lib/provider-usage apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator
  • git diff --check

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • Focused tests, desktop typecheck, Biome, and whitespace checks pass
  • "Allow edits from maintainers" is checked on fork PRs

Summary by CodeRabbit

  • New Features
    • Added an AI usage indicator to the dashboard top bar and workspace controls.
    • View remaining capacity, reset times, connection status, and account details for Claude and Codex.
    • Customize displayed metrics, privacy settings, and automatic or manual refreshes.
    • Import, add, and switch between Codex profiles.
    • Added a setting to show or hide the AI usage indicator.
  • Bug Fixes
    • Improved handling of unavailable, unconfigured, cached, and expired provider data.
  • Tests
    • Added coverage for usage collection, caching, profiles, refresh behavior, and UI states.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Claude and Codex usage collection with validation, caching, tRPC exposure, Codex profile management, persistent visibility settings, and dashboard indicators for capacity, reset times, and provider status.

Changes

Provider usage visibility

Layer / File(s) Summary
Usage contracts and router
apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts, apps/desktop/src/lib/trpc/routers/provider-usage.ts, apps/desktop/src/lib/trpc/routers/index.ts, apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
Defines validated usage snapshots and exposes provider usage queries and Codex profile mutations through tRPC.
Claude usage provider
apps/desktop/src/main/lib/provider-usage/providers/claude.ts, apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts
Resolves OAuth credentials, fetches Claude quota data, parses usage windows, and returns structured provider states.
Codex profiles and usage provider
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts, apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts, apps/desktop/src/main/lib/provider-usage/providers/codex.ts, apps/desktop/src/main/lib/provider-usage/providers/*codex*.test.ts
Stores and switches Codex profiles, runs login and app-server flows, parses rate limits, and reconciles account-aware snapshots.
Caching and collection orchestration
apps/desktop/src/main/lib/provider-usage/index.ts, apps/desktop/src/main/lib/provider-usage/index.test.ts
Collects both providers concurrently, caches snapshots for five minutes, supports forced refreshes, and deduplicates concurrent requests.
Dashboard usage indicator
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/*, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Adds polling policy helpers, metric filters, privacy controls, provider rows, refresh handling, and dashboard placement.
Usage indicator preference
apps/desktop/src/lib/trpc/routers/settings/index.ts, apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts, apps/desktop/src/shared/constants.ts, packages/local-db/drizzle/*, packages/local-db/src/schema/schema.ts
Adds the persisted AI usage indicator setting, settings procedures, search metadata, default value, and database schema migration.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AIUsageIndicator
  participant providerUsage
  participant collectProviderUsage
  participant Claude
  participant CodexAppServer

  User->>AIUsageIndicator: Open usage popover
  AIUsageIndicator->>providerUsage: getSnapshot(force)
  providerUsage->>collectProviderUsage: Collect provider snapshot
  collectProviderUsage->>Claude: Fetch OAuth usage
  collectProviderUsage->>CodexAppServer: Read rate limits
  Claude-->>collectProviderUsage: Claude usage windows
  CodexAppServer-->>collectProviderUsage: Codex rate limits
  collectProviderUsage-->>providerUsage: Cached snapshot
  providerUsage-->>AIUsageIndicator: Validated snapshot
  AIUsageIndicator-->>User: Capacity and provider rows
Loading

Possibly related PRs

Suggested reviewers: saddlepaddle

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 uses conventional commit format and clearly summarizes the main change: a multi-account AI usage meter for desktop.
Description check ✅ Passed The description includes the required change rationale, testing details, and completed checklist, with relevant privacy and implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@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: 164ab093b8

ℹ️ 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 on lines +24 to +25
const child = spawn("codex", ["app-server", "--stdio"], {
env: process.env,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the shell PATH when launching Codex

When the packaged desktop app is launched from Finder/Dock on macOS, process.env.PATH is usually the minimal GUI PATH, and the startup applyShellEnvToProcess() merge does not replace an existing PATH. This means users who have codex installed through Homebrew, npm/nvm, asdf, etc. will be reported as not configured even though codex app-server works in their terminal; the existing getProcessEnvWithShellPath/execWithShellEnv helpers are the path used elsewhere for child processes that must resolve terminal-installed CLIs.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx (1)

39-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the "unavailable" status branch.

Tests cover "ok" and "not-configured" but not "unavailable", which renders provider.errorMessage instead of the sign-in copy — a distinct, currently untested code path.

✅ Suggested additional test
test("renders the error message when usage is temporarily unavailable", () => {
	const markup = renderToStaticMarkup(
		<ProviderUsageRow
			provider={{
				...claudeProvider,
				status: "unavailable",
				windows: [],
				errorMessage: "Claude usage is temporarily unavailable.",
			}}
		/>,
	);

	expect(markup).toContain("Claude usage is temporarily unavailable.");
	expect(markup).not.toContain('role="progressbar"');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`
around lines 39 - 53, Add a test covering the unavailable status branch in
ProviderUsageRow tests, constructing the provider with status "unavailable" and
a representative errorMessage. Assert the rendered markup contains that error
message and does not contain a progressbar role, while preserving the existing
ok and not-configured coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/lib/provider-usage/providers/claude.ts`:
- Around line 18-21: Update usageBucketSchema to accept numeric as well as
string or nullish resets_at values, then update parseResetAt to normalize
numeric timestamps into the existing reset-time representation. Preserve current
handling for strings, null, and undefined, ensuring numeric endpoint values do
not invalidate the usage bucket.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`:
- Around line 39-53: Add a test covering the unavailable status branch in
ProviderUsageRow tests, constructing the provider with status "unavailable" and
a representative errorMessage. Assert the rendered markup contains that error
message and does not contain a progressbar role, while preserving the existing
ok and not-configured coverage.
🪄 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: 44ec9e76-5dfc-40e6-9843-a579fa4ff47c

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4db9c and 164ab09.

📒 Files selected for processing (20)
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts

@AlchemistChaos AlchemistChaos changed the title feat(desktop): add minimal AI usage meter feat(desktop): add multi-account AI usage meter Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (16)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts (2)

76-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename findWindowByMinutes to match its unit.

The parameter is windowSeconds and both call sites pass seconds (5 * 60 * 60 and 7 * 24 * 60 * 60). The name states minutes. Rename it to findWindowBySeconds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts`
around lines 76 - 85, Rename the function findWindowByMinutes to
findWindowBySeconds and update both call sites to use the new name, preserving
the existing windowSeconds matching behavior.

142-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compute the lowest percentage from structured values, not from formatted label strings.

getLowestSelectedRemainingPercent re-parses the numeric value out of the display strings produced by getMenuBarSummaryParts. Any future change to the label format, for example adding a % suffix or a different separator, silently breaks this calculation. Extract the selection logic into a helper that returns { label, value } pairs, then derive both the summary strings and the minimum from it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts`
around lines 142 - 151, Refactor getMenuBarSummaryParts to use a helper that
returns structured { label, value } pairs for the selected providers and
metrics. Build the displayed summary strings from the pair labels, and update
getLowestSelectedRemainingPercent to compute the minimum directly from the pair
values instead of parsing formatted strings, preserving the existing fallback
when no finite values exist.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx (1)

121-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a negative case for the Codex-only Switch button.

ProviderUsageRow.tsx line 169 renders the Switch button only when provider.providerId === "codex". This test adds an inactive Claude account but never asserts that the button is absent, so the provider guard is untested. Add expect(markup).not.toContain("Switch") to this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`
around lines 121 - 158, The test “renders inactive cached accounts separately
from the active account” should also verify that Claude providers do not render
the Codex-only Switch button. Add a negative assertion against “Switch” using
the existing markup result, while preserving the current account and
inactive-state assertions.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx (1)

77-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant fallback and reuse the derived providers value.

getLowestSelectedRemainingPercent already falls back to getLowestRemainingPercent(providers) when no metric is selected (usageIndicatorPolicy.ts line 149). The ?? branch on line 80 is therefore unreachable. Line 229 also re-reads data?.providers although line 77 already derives providers.

♻️ Proposed cleanup
 	const providers = data?.providers ?? [];
-	const remaining =
-		getLowestSelectedRemainingPercent(providers, selection) ??
-		getLowestRemainingPercent(providers);
+	const remaining = getLowestSelectedRemainingPercent(providers, selection);
-				{data?.providers.map((provider) => (
+				{providers.map((provider) => (

Then drop getLowestRemainingPercent from the import list on line 16.

Also applies to: 229-229

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx`
around lines 77 - 91, In AIUsageIndicator, simplify the remaining calculation to
use getLowestSelectedRemainingPercent(providers, selection) directly, removing
the unreachable getLowestRemainingPercent fallback and its import. At the later
usage around the component’s line 229, reuse the already-derived providers value
instead of reading data?.providers again.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx (2)

61-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the synthesized default account and check planLabel.

The whole object literal is already inside the provider.status === "ok" branch, so isActive: provider.status === "ok" on line 72 and the ternary on line 73 always evaluate to true and "ok". Line 71 also assigns provider.accountLabel to planLabel, which duplicates the account label into the plan field. planLabel is not rendered today, so there is no visible defect, but the value is wrong if a future row displays it.

♻️ Proposed cleanup
 						{
 							id: `${provider.providerId}:default`,
 							providerId: provider.providerId,
 							profileName: "default",
 							accountLabel: provider.accountLabel,
-							planLabel: provider.accountLabel,
-							isActive: provider.status === "ok",
-							status: provider.status === "ok" ? "ok" : "no-data",
+							planLabel: null,
+							isActive: true,
+							status: "ok",
 							statusMessage: provider.errorMessage,
 							windows: provider.windows,
 						} satisfies ProviderUsageAccount,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx`
around lines 61 - 78, In the synthesized default account within
ProviderUsageRow, simplify the status fields to their branch-invariant values:
set isActive to true and status to "ok". Do not copy provider.accountLabel into
planLabel; use the appropriate absent/default plan value while preserving the
existing accountLabel and other account properties.

139-224: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two files each declare a second React component. The coding guidelines require one component per file, organized as ComponentName/ComponentName.tsx with an index.ts barrel export. Both new files declare a helper component alongside the exported one.

  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx#L139-L224: move AccountUsageBlock into components/AccountUsageBlock/AccountUsageBlock.tsx with an index.ts barrel export, co-located under ProviderUsageRow since it is its only consumer.
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx#L267-L290: move MetricToggle into components/MetricToggle/MetricToggle.tsx with an index.ts barrel export, co-located under AIUsageIndicator.

As per coding guidelines: "Use one component per file and organize components as ComponentName/ComponentName.tsx with an index.ts barrel export" and "Co-locate components, utilities, hooks, constants, configuration, tests, and stories with the code that uses them."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx`
around lines 139 - 224, Move AccountUsageBlock from
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx:139-224
into a co-located components/AccountUsageBlock/AccountUsageBlock.tsx with an
index.ts barrel export, and update ProviderUsageRow imports. Move MetricToggle
from
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx:267-290
into components/MetricToggle/MetricToggle.tsx with an index.ts barrel export,
and update AIUsageIndicator imports; leave each parent file with only its
primary component.

Source: Coding guidelines

apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts (1)

118-178: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case where the active account windows differ from the provider windows.

In every fixture the account windows either match provider.windows or the accounts array is empty. The precedence rule in activeWindows (usageIndicatorPolicy.ts lines 56-65), where active-account windows win over provider windows with the same id, is therefore never exercised. Add one provider whose active account reports a different remainingPercent for the same window id, and assert the account value is used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts`
around lines 118 - 178, Add a test fixture with an active account window sharing
an id with a provider window but having a different remainingPercent, then
assert the relevant summary or lowest-remaining result uses the account value.
Keep the provider window and active account association explicit so
activeWindows precedence for the matching id is exercised.
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts (3)

154-191: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case for a window with resetAt: null.

projectCachedWindows keeps a window when window.resetAt === null. No test covers that branch. The branch decides whether a window with no reset time stays visible forever, which is the opposite of the "no fabricated capacity" intent this test states. Add a third window with resetAt: null and assert it survives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts`
around lines 154 - 191, Add a third window with resetAt: null to the snapshot in
the projectCachedWindows test, then include that window unchanged in the
expected result alongside the non-expired secondary window. This should cover
the branch that preserves windows without a reset time.

77-91: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider asserting the file mode on the imported credential.

The test verifies the content of the stored auth.json but not its permissions. copySensitiveFileAtomically applies SUPERSET_SENSITIVE_FILE_MODE, which is the security property that matters for a stored OAuth token. A statSync(...).mode & 0o777 assertion would prevent a silent regression if the chmod step is ever dropped.

♻️ Proposed addition
+		expect(
+			statSync(
+				join(
+					rootDir,
+					"provider-usage",
+					"codex",
+					"profiles",
+					"person-example-com",
+					"auth.json",
+				),
+			).mode & 0o777,
+		).toBe(SUPERSET_SENSITIVE_FILE_MODE);

Import statSync from node:fs and SUPERSET_SENSITIVE_FILE_MODE from main/lib/app-environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts`
around lines 77 - 91, Extend the credential test around parseCodexIdentity to
import statSync and SUPERSET_SENSITIVE_FILE_MODE, then assert the stored
auth.json permission bits (statSync(...).mode & 0o777) equal
SUPERSET_SENSITIVE_FILE_MODE, alongside the existing accountId assertion.

192-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering putSnapshot and accountRows.

The store exposes two behaviors with real logic and no test:

  • putSnapshot drops a write when an existing snapshot has a capturedAt greater than or equal to the new one. A regression here would silently freeze usage data.
  • accountRows builds the exact ProviderUsageAccount contract that the dashboard renders, including the ok/cached/no-data status mapping.

Both accept the injected rootDir, so they need no new test scaffolding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts` at
line 192, Add tests covering the store’s putSnapshot and accountRows behaviors
using the existing injected rootDir setup. Verify putSnapshot rejects writes
whose capturedAt is less than or equal to the existing snapshot and accepts
newer snapshots, then verify accountRows produces the expected
ProviderUsageAccount fields and ok/cached/no-data status mapping.
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts (3)

382-409: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Read the snapshots file once, and use async/await here.

Two points:

  • getSnapshot at line 385 runs inside the map, and each call re-reads and re-parses the whole snapshots.json file. With N profiles this performs N synchronous reads of the same file on the Electron main thread.
  • This is the only function in the file that uses a .then() chain. Every other function uses async/await.
♻️ Proposed refactor
-	function accountRows(): Promise<ProviderUsageAccount[]> {
-		return listProfiles().then((profiles) =>
-			profiles.map((profile) => {
-				const snapshot = getSnapshot(profile.identity.accountId);
-				const windows = snapshot ? projectCachedWindows(snapshot) : [];
+	async function accountRows(): Promise<ProviderUsageAccount[]> {
+		const profiles = await listProfiles();
+		const snapshots = readSnapshots();
+		return profiles.map((profile) => {
+			const snapshot = snapshots[profile.identity.accountId] ?? null;
+			const windows = snapshot ? projectCachedWindows(snapshot) : [];
 			const hasCachedWindows = windows.length > 0;
 			return {
 				id: `codex:${profile.identity.accountId}`,
 				providerId: "codex" as const,
 				profileName: profile.profileName,
 				accountLabel: profile.identity.email,
 				planLabel: snapshot?.planLabel ?? profile.identity.plan,
 				isActive: profile.isActive,
 				status: hasCachedWindows
 					? profile.isActive
 						? "ok"
 						: "cached"
 					: "no-data",
 				statusMessage: hasCachedWindows
 					? profile.isActive
 						? "live"
 						: "cached"
 					: "Use Codex once to record limits",
 				windows,
 			};
-			}),
-		);
+		});
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 382 - 409, Refactor accountRows() to async/await and load the snapshots
data once before iterating profiles, then resolve each profile’s cached snapshot
from that in-memory data instead of calling getSnapshot() inside map. Preserve
the existing account-row fields, status logic, and empty-snapshot behavior while
matching the file’s established async/await style.

74-96: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Use the async rename in these async helpers.

Both helpers are async but call renameSync. This blocks the Electron main thread. Import rename from node:fs/promises to keep the whole path non-blocking.

♻️ Proposed change
-import {
-	copyFile,
-	mkdir,
-	readdir,
-	readFile,
-	writeFile,
-} from "node:fs/promises";
+import {
+	copyFile,
+	mkdir,
+	readdir,
+	readFile,
+	rename,
+	writeFile,
+} from "node:fs/promises";
 	await copyFile(source, tempPath);
 	chmodSensitive(tempPath);
-	renameSync(tempPath, destination);
+	await rename(tempPath, destination);
 	chmodSensitive(destination);
 	await writeFile(tempPath, contents, {
 		mode: SUPERSET_SENSITIVE_FILE_MODE,
 	});
 	chmodSensitive(tempPath);
-	renameSync(tempPath, destination);
+	await rename(tempPath, destination);
 	chmodSensitive(destination);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 74 - 96, Replace the synchronous renameSync calls in
copySensitiveFileAtomically and writeSensitiveFileAtomically with the awaited
async rename from node:fs/promises, adding the required import and preserving
the existing atomic operation order.

205-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename CCM_CODEX_EXECUTABLE to SUPERSET_CODEX_EXECUTABLE, or document why the CCM_ prefix is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 205 - 212, Update findCodexExecutable to read SUPERSET_CODEX_EXECUTABLE
instead of CCM_CODEX_EXECUTABLE, unless the CCM_ prefix is intentionally
required; if retaining it, document that intent near the environment-variable
lookup.
apps/desktop/src/main/lib/provider-usage/providers/codex.ts (2)

38-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider narrowing the profileStore dependency type.

profileStore?: typeof codexProfileStore requires the full concrete store shape. collectCodexUsage only calls activeIdentity, accountRows, and putSnapshot. The test fake in codex.test.ts must still stub importActive, activate, addViaIsolatedLogin, listProfiles, getSnapshot, and activeAuthPath just to satisfy the type. Every future store method will force the same churn.

Declare only the members this module consumes.

♻️ Proposed change
+interface CodexProfileStoreAccess {
+	activeIdentity: () => CodexIdentity | null;
+	accountRows: () => Promise<ProviderUsageAccount[]>;
+	putSnapshot: (snapshot: CodexUsageSnapshot) => Promise<void>;
+}
+
 interface CodexUsageDependencies {
 	readRateLimits?: () => Promise<CodexRateLimitsReadResult>;
-	profileStore?: typeof codexProfileStore;
+	profileStore?: CodexProfileStoreAccess;
 	now?: () => number;
 }

Import CodexUsageSnapshot as a type from ./codex-profiles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.ts` around lines 38
- 42, Update the CodexUsageDependencies.profileStore type to a minimal interface
containing only activeIdentity, accountRows, and putSnapshot, using the
CodexUsageSnapshot type imported from ./codex-profiles as needed. Keep
collectCodexUsage behavior unchanged and allow test fakes to implement only
these consumed members.

213-292: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a result builder for the repeated ProviderUsage literals.

collectCodexUsage returns six near-identical object literals. All six repeat providerId: "codex" and providerName: "Codex", and most repeat the active?.accountLabel ?? ... and active?.windows ?? [] expressions. The differences are limited to status, accountLabel, windows, and errorMessage.

A small helper would shorten the function and make the branch differences visible at a glance.

♻️ Sketch
+function codexResult(
+	accounts: ProviderUsageAccount[],
+	overrides: Partial<ProviderUsage> = {},
+): ProviderUsage {
+	const active = activeAccountFrom(accounts);
+	return {
+		providerId: "codex",
+		providerName: "Codex",
+		status: statusFromAccounts(accounts),
+		accountLabel: active?.accountLabel ?? active?.planLabel ?? null,
+		activeAccountId: active?.id ?? null,
+		accounts,
+		windows: active?.windows ?? [],
+		errorMessage: null,
+		...overrides,
+	};
+}

Each return then narrows to the fields that differ, for example:

-	return {
-		providerId: "codex",
-		providerName: "Codex",
-		status: "unavailable",
-		accountLabel: active?.accountLabel ?? null,
-		activeAccountId: active?.id ?? null,
-		accounts,
-		windows: [],
-		errorMessage: "Codex usage is temporarily unavailable.",
-	};
+	return codexResult(accounts, {
+		status: "unavailable",
+		windows: [],
+		errorMessage: "Codex usage is temporarily unavailable.",
+	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.ts` around lines 213
- 292, Extract a local result-builder helper for the repeated ProviderUsage
objects in collectCodexUsage, centralizing providerId and providerName and
accepting the varying status, account label, windows, and errorMessage values.
Replace each existing return literal with calls to this helper while preserving
the current branch-specific fallbacks and values.
apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts (1)

150-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the complementary test for a successful snapshot write.

This test proves that no snapshot is cached when the active identity changes. The positive case is not covered: when the identity is unchanged, collectCodexUsage must call putSnapshot with the parsed windows, the parsed planLabel, and the injected now() value.

Without that test, a regression that stops persisting snapshots entirely would still pass the suite, because the only putSnapshot assertion is toEqual([]).

💚 Sketch
+	test("caches the live reading when the active account is stable", async () => {
+		const identity: CodexIdentity = {
+			accountId: "acct_a",
+			email: "first@example.com",
+			plan: "pro",
+		};
+		const written: CodexUsageSnapshot[] = [];
+		const store: typeof codexProfileStore = {
+			...emptyProfileStore(),
+			activeIdentity: () => identity,
+			putSnapshot: async (snapshot) => {
+				written.push(snapshot);
+			},
+		};
+
+		await collectCodexUsage({
+			profileStore: store,
+			now: () => 1_700_000_000_000,
+			readRateLimits: async () => ({
+				status: "ok",
+				value: {
+					rateLimits: { planType: "pro", primary: { usedPercent: 20 } },
+				},
+			}),
+		});
+
+		expect(written).toHaveLength(1);
+		expect(written[0]?.accountId).toBe("acct_a");
+		expect(written[0]?.capturedAt).toBe(1_700_000_000_000);
+		expect(written[0]?.planLabel).toBe("PRO");
+		expect(written[0]?.windows).toHaveLength(1);
+	});

Import CodexUsageSnapshot as a type from ./codex-profiles.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts` around
lines 150 - 204, Add a complementary test near the existing
active-account-change case that keeps the identity unchanged and verifies
successful snapshot persistence. Configure the test store to capture the written
snapshot, provide parsed rate-limit windows, plan label, and an injected now()
value, then assert putSnapshot receives a CodexUsageSnapshot containing those
values. Import CodexUsageSnapshot as a type from ./codex-profiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/provider-usage.ts`:
- Around line 72-78: Update refreshAfterMutation so failures from
resolved.collect({ force: true }) are caught and do not propagate after mutation
succeeds. Always return the mutation result from refreshAfterMutation, while
still invoking the forced collection so existing behavior and tests remain
intact.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts`:
- Around line 63-71: Update the environment-loading flow around getEnv and
defaultCodexHomeEnv to retain whether environment resolution failed, then use
that state in the Codex startup ENOENT handling to return unavailable when
executable availability is unknown instead of not-configured. Add coverage for
rejected getEnv followed by an ENOENT startup error.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts`:
- Around line 426-453: Update the login process around the spawned child in the
Codex profile flow to consume child.stderr while it runs, and include the
collected stderr text in the non-zero-exit error from the exit handler. Preserve
timeout cleanup and rejection behavior, and ensure the timeout terminates
unresponsive processes forcefully if the CLI can ignore the default termination
signal.
- Around line 354-367: Update readSnapshots to validate parsed JSON with the
existing Zod dependency and the provider-usage schema before returning it.
Derive or reuse a schema for CodexUsageSnapshot, including validating
UsageWindow entries, and retain only entries that successfully parse; return an
empty record when the top-level value is invalid. Keep getSnapshot unchanged so
projectCachedWindows never receives malformed snapshots.
- Around line 74-83: Update copySensitiveFileAtomically and
writeSensitiveFileAtomically to clean up their temporary paths when copying,
permission changes, or renaming fails. Ensure cleanup is attempted in a catch or
finally block without masking the original error, while preserving the existing
atomic write behavior on success.
- Around line 339-347: Update the backup flow around copySensitiveFileAtomically
to prune backupsDir after each successful backup, retaining only a small fixed
number of the newest auth-*.json files and deleting older backups. Ensure
pruning is limited to matching backup files and preserves the newly created
backup.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.ts`:
- Around line 106-122: Unify Codex profile-name derivation by exporting the
normalization helper used by accountRows in
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts:382-409,
then reuse it in currentAccountRow in
apps/desktop/src/main/lib/provider-usage/providers/codex.ts:106-122 instead of
using the raw email fallback, so both paths produce the same normalized
profileName.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx`:
- Around line 100-112: Update the importCodex, addCodex, and switchCodex
mutations with onError handlers that store or expose each failure message, then
render the resulting error in the Codex popover. Ensure inline error text
includes the select-text cursor-text classes, while preserving the existing
success invalidation behavior.
- Line 25: Update the persisted-key registry test data to include
superset.aiUsage.metricSelection, documenting its fixed four-boolean bound,
deletion owner or path, and removal behavior. Ensure the registry entry matches
the key used by AIUsageIndicator and follows the existing persisted-key
test-data structure.

---

Nitpick comments:
In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts`:
- Around line 154-191: Add a third window with resetAt: null to the snapshot in
the projectCachedWindows test, then include that window unchanged in the
expected result alongside the non-expired secondary window. This should cover
the branch that preserves windows without a reset time.
- Around line 77-91: Extend the credential test around parseCodexIdentity to
import statSync and SUPERSET_SENSITIVE_FILE_MODE, then assert the stored
auth.json permission bits (statSync(...).mode & 0o777) equal
SUPERSET_SENSITIVE_FILE_MODE, alongside the existing accountId assertion.
- Line 192: Add tests covering the store’s putSnapshot and accountRows behaviors
using the existing injected rootDir setup. Verify putSnapshot rejects writes
whose capturedAt is less than or equal to the existing snapshot and accepts
newer snapshots, then verify accountRows produces the expected
ProviderUsageAccount fields and ok/cached/no-data status mapping.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts`:
- Around line 382-409: Refactor accountRows() to async/await and load the
snapshots data once before iterating profiles, then resolve each profile’s
cached snapshot from that in-memory data instead of calling getSnapshot() inside
map. Preserve the existing account-row fields, status logic, and empty-snapshot
behavior while matching the file’s established async/await style.
- Around line 74-96: Replace the synchronous renameSync calls in
copySensitiveFileAtomically and writeSensitiveFileAtomically with the awaited
async rename from node:fs/promises, adding the required import and preserving
the existing atomic operation order.
- Around line 205-212: Update findCodexExecutable to read
SUPERSET_CODEX_EXECUTABLE instead of CCM_CODEX_EXECUTABLE, unless the CCM_
prefix is intentionally required; if retaining it, document that intent near the
environment-variable lookup.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts`:
- Around line 150-204: Add a complementary test near the existing
active-account-change case that keeps the identity unchanged and verifies
successful snapshot persistence. Configure the test store to capture the written
snapshot, provide parsed rate-limit windows, plan label, and an injected now()
value, then assert putSnapshot receives a CodexUsageSnapshot containing those
values. Import CodexUsageSnapshot as a type from ./codex-profiles.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.ts`:
- Around line 38-42: Update the CodexUsageDependencies.profileStore type to a
minimal interface containing only activeIdentity, accountRows, and putSnapshot,
using the CodexUsageSnapshot type imported from ./codex-profiles as needed. Keep
collectCodexUsage behavior unchanged and allow test fakes to implement only
these consumed members.
- Around line 213-292: Extract a local result-builder helper for the repeated
ProviderUsage objects in collectCodexUsage, centralizing providerId and
providerName and accepting the varying status, account label, windows, and
errorMessage values. Replace each existing return literal with calls to this
helper while preserving the current branch-specific fallbacks and values.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx`:
- Around line 77-91: In AIUsageIndicator, simplify the remaining calculation to
use getLowestSelectedRemainingPercent(providers, selection) directly, removing
the unreachable getLowestRemainingPercent fallback and its import. At the later
usage around the component’s line 229, reuse the already-derived providers value
instead of reading data?.providers again.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`:
- Around line 121-158: The test “renders inactive cached accounts separately
from the active account” should also verify that Claude providers do not render
the Codex-only Switch button. Add a negative assertion against “Switch” using
the existing markup result, while preserving the current account and
inactive-state assertions.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx`:
- Around line 61-78: In the synthesized default account within ProviderUsageRow,
simplify the status fields to their branch-invariant values: set isActive to
true and status to "ok". Do not copy provider.accountLabel into planLabel; use
the appropriate absent/default plan value while preserving the existing
accountLabel and other account properties.
- Around line 139-224: Move AccountUsageBlock from
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx:139-224
into a co-located components/AccountUsageBlock/AccountUsageBlock.tsx with an
index.ts barrel export, and update ProviderUsageRow imports. Move MetricToggle
from
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx:267-290
into components/MetricToggle/MetricToggle.tsx with an index.ts barrel export,
and update AIUsageIndicator imports; leave each parent file with only its
primary component.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts`:
- Around line 118-178: Add a test fixture with an active account window sharing
an id with a provider window but having a different remainingPercent, then
assert the relevant summary or lowest-remaining result uses the account value.
Keep the provider window and active account association explicit so
activeWindows precedence for the matching id is exercised.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts`:
- Around line 76-85: Rename the function findWindowByMinutes to
findWindowBySeconds and update both call sites to use the new name, preserving
the existing windowSeconds matching behavior.
- Around line 142-151: Refactor getMenuBarSummaryParts to use a helper that
returns structured { label, value } pairs for the selected providers and
metrics. Build the displayed summary strings from the pair labels, and update
getLowestSelectedRemainingPercent to compute the minimum directly from the pair
values instead of parsing formatted strings, preserving the existing fallback
when no finite values exist.
🪄 Autofix

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 Plus

Run ID: 0b27558e-1b18-4b7f-b4e2-df7d69e60296

📥 Commits

Reviewing files that changed from the base of the PR and between 164ab09 and b35f327.

📒 Files selected for processing (18)
  • apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts

Comment on lines +354 to +367
function readSnapshots(): Record<string, CodexUsageSnapshot> {
try {
return JSON.parse(readFileSync(snapshotsPath, "utf8")) as Record<
string,
CodexUsageSnapshot
>;
} catch {
return {};
}
}

function getSnapshot(accountId: string): CodexUsageSnapshot | null {
return readSnapshots()[accountId] ?? null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the parsed snapshots file before use.

readSnapshots casts the parsed JSON straight to Record<string, CodexUsageSnapshot> with no validation. JSON.parse succeeds for any well-formed JSON, so a truncated write, a hand edit, or a snapshot written by an older schema version all pass through. projectCachedWindows then calls snapshot.windows.filter(...) at line 200. If windows is not an array, this throws a TypeError that propagates out of accountRows and fails the whole usage collection.

The file already depends on Zod through provider-usage.schema. Validate the shape here and drop entries that do not match.

🛡️ Proposed fix using a defensive guard
 	function readSnapshots(): Record<string, CodexUsageSnapshot> {
 		try {
-			return JSON.parse(readFileSync(snapshotsPath, "utf8")) as Record<
-				string,
-				CodexUsageSnapshot
-			>;
+			const parsed: unknown = JSON.parse(readFileSync(snapshotsPath, "utf8"));
+			if (!parsed || typeof parsed !== "object") return {};
+			const result: Record<string, CodexUsageSnapshot> = {};
+			for (const [accountId, value] of Object.entries(parsed)) {
+				const candidate = value as Partial<CodexUsageSnapshot>;
+				if (
+					typeof candidate?.accountId === "string" &&
+					typeof candidate.capturedAt === "number" &&
+					Array.isArray(candidate.windows)
+				) {
+					result[accountId] = candidate as CodexUsageSnapshot;
+				}
+			}
+			return result;
 		} catch {
 			return {};
 		}
 	}

A Zod schema derived from UsageWindow would be stronger, because it would also reject malformed window entries.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function readSnapshots(): Record<string, CodexUsageSnapshot> {
try {
return JSON.parse(readFileSync(snapshotsPath, "utf8")) as Record<
string,
CodexUsageSnapshot
>;
} catch {
return {};
}
}
function getSnapshot(accountId: string): CodexUsageSnapshot | null {
return readSnapshots()[accountId] ?? null;
}
function readSnapshots(): Record<string, CodexUsageSnapshot> {
try {
const parsed: unknown = JSON.parse(readFileSync(snapshotsPath, "utf8"));
if (!parsed || typeof parsed !== "object") return {};
const result: Record<string, CodexUsageSnapshot> = {};
for (const [accountId, value] of Object.entries(parsed)) {
const candidate = value as Partial<CodexUsageSnapshot>;
if (
typeof candidate?.accountId === "string" &&
typeof candidate.capturedAt === "number" &&
Array.isArray(candidate.windows)
) {
result[accountId] = candidate as CodexUsageSnapshot;
}
}
return result;
} catch {
return {};
}
}
function getSnapshot(accountId: string): CodexUsageSnapshot | null {
return readSnapshots()[accountId] ?? null;
}
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 354 - 367, Update readSnapshots to validate parsed JSON with the existing
Zod dependency and the provider-usage schema before returning it. Derive or
reuse a schema for CodexUsageSnapshot, including validating UsageWindow entries,
and retain only entries that successfully parse; return an empty record when the
top-level value is invalid. Keep getSnapshot unchanged so projectCachedWindows
never receives malformed snapshots.

Comment on lines +106 to +122
function currentAccountRow(
identity: CodexIdentity,
windows: UsageWindow[],
planLabel: string | null,
): ProviderUsageAccount {
return {
id: `codex:${identity.accountId}`,
providerId: "codex",
profileName: identity.email ?? "current",
accountLabel: identity.email,
planLabel: planLabel ?? identity.plan,
isActive: true,
status: windows.length > 0 ? "ok" : "no-data",
statusMessage: windows.length > 0 ? "live" : "No reading yet",
windows,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

profileName uses two different formats for the same Codex account. Both files build a ProviderUsageAccount, but each derives profileName differently. The shared root cause is that no single function owns the profile-name format. An account that is active but not yet imported shows one label, then shows a different label after import, with no user action in between.

  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts#L106-L122: currentAccountRow sets profileName: identity.email ?? "current", so a raw email address. Derive the name with the same normalization the profile store uses, or set a clearly provisional value that the renderer can distinguish.
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts#L382-L409: accountRows sets profileName: profile.profileName, the normalized directory slug such as person-example-com. Export the normalization helper so codex.ts can reuse it and both paths agree.
📍 Affects 2 files
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts#L106-L122 (this comment)
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts#L382-L409
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex.ts` around lines 106
- 122, Unify Codex profile-name derivation by exporting the normalization helper
used by accountRows in
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts:382-409,
then reuse it in currentAccountRow in
apps/desktop/src/main/lib/provider-usage/providers/codex.ts:106-122 instead of
using the raw email fallback, so both paths produce the same normalized
profileName.

type UsageMetricSelection,
} from "./usageIndicatorPolicy";

const METRIC_SELECTION_STORAGE_KEY = "superset.aiUsage.metricSelection";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the new localStorage key is registered in the persisted key registry.
set -uo pipefail

fd -i 'persisted-key-registry' apps/desktop/src

# Test: the new key must appear in the registry test data. Expect: at least one match.
rg -n 'aiUsage|metricSelection' apps/desktop/src/renderer/lib/persisted-keys/ || echo "NOT REGISTERED"

# Show how existing keys are declared and documented.
fd -i 'persisted-key' apps/desktop/src --exec rg -n -C3 'superset\.' {}

Repository: superset-sh/superset

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target file ---'
sed -n '1,180p' apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx

printf '%s\n' '--- persisted-key candidates ---'
fd -i 'persisted|storage|registry' apps/desktop/src apps/desktop 2>/dev/null | head -200

printf '%s\n' '--- relevant references ---'
rg -n -C3 'localStorage|persisted-key|metricSelection|aiUsage' apps/desktop/src apps/desktop 2>/dev/null | head -300

Repository: superset-sh/superset

Length of output: 39042


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all persisted-key registry paths ---'
git ls-files | rg -i 'persisted[-_]?key|localstorage|storage.*registry|registry.*storage' || true

printf '%s\n' '--- all registry and allowlist references ---'
rg -n -i 'persisted[- ]key|localStorage.*allowlist|allowlist.*localStorage|storage key|deletion owner|feature.*removed' . \
  -g '!**/node_modules/**' -g '!**/.git/**' | head -400 || true

printf '%s\n' '--- exact metric key usage ---'
rg -n -C5 'superset\.aiUsage\.metricSelection|METRIC_SELECTION_STORAGE_KEY' apps/desktop/src

Repository: superset-sh/superset

Length of output: 5427


Add and document the persisted-key registry entry.

AIUsageIndicator reads and writes superset.aiUsage.metricSelection. Add apps/desktop/src/renderer/lib/persisted-keys/persisted-key-registry.test-data.ts and register this key with its fixed four-boolean bound, deletion owner or path, and removal behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx`
at line 25, Update the persisted-key registry test data to include
superset.aiUsage.metricSelection, documenting its fixed four-boolean bound,
deletion owner or path, and removal behavior. Ensure the registry entry matches
the key used by AIUsageIndicator and follows the existing persisted-key
test-data structure.

Source: Path instructions

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 22 files

Architecture diagram
sequenceDiagram
    participant UI as AIUsageIndicator (TopBar)
    participant Policy as usageIndicatorPolicy
    participant IPC as tRPC Router (providerUsage)
    participant Collector as ProviderUsageCollector (Main)
    participant Cache as Five-Minute Cache
    participant Claude as Claude Provider
    participant Codex as Codex Provider
    participant ProfileStore as CodexProfileStore
    participant AppServer as codex app-server (subprocess)
    participant Anthropic as Anthropic API
    participant Keychain as macOS Keychain / Superset Credentials
    participant FS as Filesystem (~/.codex, app-owned dirs)

    Note over UI,FS: Renderer (network-cold) <-> Main process boundary

    rect rgb(240, 248, 255)
        Note over UI,Policy: Renderer - network-cold until opened
        UI->>Policy: shouldQueryProviderUsage(open)
        Policy-->>UI: false (closed) / true (open)

        alt Popover closed
            Note over UI: No IPC call, no polling
        else Popover opened
            UI->>IPC: getSnapshot.query()
            Note over UI: refetchInterval set to 5 min while open
        end

        UI->>Policy: getMenuBarSummaryParts(providers, selection)
        Policy-->>UI: "A 72 · C 40" compact label
        UI->>Policy: getLowestSelectedRemainingPercent()
        Policy-->>UI: lowest remaining %
    end

    rect rgb(255, 250, 240)
        Note over IPC,Collector: Main process - caching layer
        IPC->>Collector: collect({ force: false })
        Collector->>Cache: Check TTL (5 min)
        alt Cache valid and !force
            Cache-->>Collector: cached snapshot
        else Cache expired or force
            Collector->>Collector: inFlight deduplication check
            par Claude and Codex in parallel
                Collector->>Claude: collectClaudeUsage()
                Collector->>Codex: collectCodexUsage()
            end
            Cache-->>Collector: new snapshot
        end
        Collector-->>IPC: ProviderUsageSnapshot (Zod-validated)
        IPC-->>UI: Safe fields only (no credentials)
    end

    rect rgb(245, 245, 255)
        Note over Claude,Keychain: Claude Provider

        Claude->>Keychain: readCredentials()
        alt Not configured
            Keychain-->>Claude: null
            Claude-->>Collector: status=not-configured
        else Has valid OAuth (Keychain or Superset creds)
            Claude->>Anthropic: GET /api/oauth/usage (Bearer token)
            alt 200 OK with usage buckets
                Anthropic-->>Claude: five_hour, seven_day, limits[]
                Claude-->>Collector: status=ok, windows[]
            else Non-200 or malformed
                Anthropic-->>Claude: error/429
                Claude-->>Collector: status=unavailable, safe errorMessage
            end
        end
    end

    rect rgb(240, 255, 240)
        Note over Codex,FS: Codex Provider (local subprocess)

        Codex->>ProfileStore: activeIdentity()
        ProfileStore->>FS: read ~/.codex/auth.json
        FS-->>ProfileStore: identity (accountId, email, plan)

        Codex->>AppServer: spawn codex app-server --stdio
        Note over AppServer: Env strips CODEX_HOME / CODEX_SQLITE_HOME
        
        AppServer->>AppServer: initialize (id=1)
        AppServer-->>Codex: initialize response
        AppServer->>AppServer: initialized notification
        AppServer->>AppServer: account/rateLimits/read (id=2)
        AppServer-->>Codex: rateLimits result
        
        alt Executable not found (ENOENT)
            AppServer-->>Codex: status=not-configured
        else Success
            Codex->>ProfileStore: confirmedActiveIdentity()
            alt Identity stable during poll
                Codex->>ProfileStore: putSnapshot(accountId, capturedAt, windows)
            end
            ProfileStore->>ProfileStore: projectCachedWindows() - filters expired
            Codex-->>Collector: status=ok, windows[], accounts[]
        end
    end

    rect rgb(255, 245, 255)
        Note over UI,ProfileStore: Account Mutations (user-triggered)

        UI->>IPC: importCurrentCodex.mutation()
        IPC->>ProfileStore: importActive()
        ProfileStore->>FS: copy auth.json -> app-owned provider-usage dir
        Note over ProfileStore: temp-file + rename, chmod sensitive
        
        UI->>IPC: addCodexAccount.mutation()
        IPC->>ProfileStore: addViaIsolatedLogin()
        ProfileStore->>FS: isolated CODEX_HOME login flow

        UI->>IPC: switchCodexProfile(profileName)
        IPC->>IPC: Regex guard /^[a-z0-9][a-z0-9-]*$/
        IPC->>ProfileStore: activate(profileName)
        ProfileStore->>FS: backup active auth.json to backups dir
        ProfileStore->>FS: atomic replace ~/.codex/auth.json

        Note over IPC,Collector: Each mutation triggers force refresh
        IPC->>Collector: collect({ force: true })
        Collector-->>IPC: updated snapshot (accounts reflect change)
        IPC-->>UI: mutation result + invalidate query
    end

    rect rgb(250, 250, 250)
        Note over UI,Policy: Renderer - error/window states

        UI->>Policy: formatResetLabel(resetAt)
        Policy-->>UI: "3h 42m · Jul 21, 6:30 PM"

        alt Provider not-configured
            UI->>UI: Show "Sign in with Claude/Codex CLI"
        else Provider unavailable
            UI->>UI: Show generic errorMessage, no fabricated capacity
        else Provider ok but account cached
            UI->>UI: Show cached status with "Available after using this account"
        end
    end
Loading

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/lib/provider-usage/index.test.ts`:
- Around line 116-122: Update the test around collect() so the initial poll
remains pending after starting collect({ force: true }); assert claudeCalls is
still 1 before resolving the first poll, then call resolveClaude and retain the
existing result and call-count assertions to verify the forced poll waits.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx`:
- Around line 46-51: Update the Codex inactive-account control in
AccountUsageBlock so it is only rendered when onSwitchProfile exists, or is
disabled when that callback is unavailable. Preserve the existing
actionsDisabled behavior and profile-switch callback invocation when the handler
is provided.
🪄 Autofix

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 Plus

Run ID: f1da826f-13f0-4a24-896f-6aebdc2f9fb6

📥 Commits

Reviewing files that changed from the base of the PR and between b35f327 and 9f620b5.

📒 Files selected for processing (20)
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/MetricToggle.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 20 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx (1)

291-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a fixed expired timestamp.

Line 318 uses Date.now() even though this test only needs an already-expired window. Use resetAt: 0 so the test has no wall-clock dependency.

Proposed fix
-									resetAt: Date.now() - 1_000,
+									resetAt: 0,

Based on learnings: “test code should avoid real wall-clock timers.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`
around lines 291 - 330, Replace the Date.now()-based resetAt value in the
“filters expired cached account windows at render time” test with the fixed
expired timestamp 0, leaving the rest of the test unchanged.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx`:
- Around line 291-330: Replace the Date.now()-based resetAt value in the
“filters expired cached account windows at render time” test with the fixed
expired timestamp 0, leaving the rest of the test unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5815f35-061b-4950-aa8d-4fc5fceb31bb

📥 Commits

Reviewing files that changed from the base of the PR and between 9f620b5 and 7ef2a76.

📒 Files selected for processing (7)
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 7 files (changes from recent commits).

You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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="apps/desktop/src/main/lib/provider-usage/index.ts">

<violation number="1" location="apps/desktop/src/main/lib/provider-usage/index.ts:85">
P2: Concurrent forced refreshes can still launch duplicate provider polls: `inFlight` is cleared before the coalesced `forcedFollowUp` begins, so a force request in that interval skips this guard and calls `collectFresh()` independently. Checking `forcedFollowUp` before the `inFlight` branch (or otherwise serializing that transition) keeps all force requests on the same follow-up and prevents competing snapshots.

(Based on your team's feedback about preserving forced follow-up requests across overlapping scans.)</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx:28">
P2: When every returned window has expired, this row now hides the progress bar but displays `live` as its usage message (and in the account header), implying a valid live reading when none is visible. The empty-state message should distinguish a successful fetch with no active windows from an account that has a current reading.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

}
if (inFlight) {
if (!options.force) return inFlight;
if (forcedFollowUp) return forcedFollowUp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Concurrent forced refreshes can still launch duplicate provider polls: inFlight is cleared before the coalesced forcedFollowUp begins, so a force request in that interval skips this guard and calls collectFresh() independently. Checking forcedFollowUp before the inFlight branch (or otherwise serializing that transition) keeps all force requests on the same follow-up and prevents competing snapshots.

(Based on your team's feedback about preserving forced follow-up requests across overlapping scans.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/provider-usage/index.ts, line 85:

<comment>Concurrent forced refreshes can still launch duplicate provider polls: `inFlight` is cleared before the coalesced `forcedFollowUp` begins, so a force request in that interval skips this guard and calls `collectFresh()` independently. Checking `forcedFollowUp` before the `inFlight` branch (or otherwise serializing that transition) keeps all force requests on the same follow-up and prevents competing snapshots.

(Based on your team's feedback about preserving forced follow-up requests across overlapping scans.) </comment>

<file context>
@@ -80,7 +82,15 @@ export function createProviderUsageCollector(
 		if (inFlight) {
 			if (!options.force) return inFlight;
-			await inFlight.catch(() => undefined);
+			if (forcedFollowUp) return forcedFollowUp;
+			const currentPoll = inFlight;
+			forcedFollowUp = currentPoll
</file context>

? "account"
: (account.accountLabel ?? account.profileName);
const now = Date.now();
const visibleWindows = account.windows.filter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When every returned window has expired, this row now hides the progress bar but displays live as its usage message (and in the account header), implying a valid live reading when none is visible. The empty-state message should distinguish a successful fetch with no active windows from an account that has a current reading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx, line 28:

<comment>When every returned window has expired, this row now hides the progress bar but displays `live` as its usage message (and in the account header), implying a valid live reading when none is visible. The empty-state message should distinguish a successful fetch with no active windows from an account that has a current reading.</comment>

<file context>
@@ -24,6 +24,10 @@ export function AccountUsageBlock({
 		? "account"
 		: (account.accountLabel ?? account.profileName);
+	const now = Date.now();
+	const visibleWindows = account.windows.filter(
+		(window) => window.resetAt === null || window.resetAt > now,
+	);
</file context>

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts (2)

403-412: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the parsed snapshots file before use.

readSnapshots still casts the parsed JSON directly to Record<string, CodexUsageSnapshot>. JSON.parse accepts any well-formed JSON, so a truncated write or an older schema version passes through. projectCachedWindows then calls snapshot.windows.filter(...) at Line 211. If windows is not an array, the TypeError propagates out of accountRows and fails the whole usage collection.

🛡️ Proposed fix
 	function readSnapshots(): Record<string, CodexUsageSnapshot> {
 		try {
-			return JSON.parse(readFileSync(snapshotsPath, "utf8")) as Record<
-				string,
-				CodexUsageSnapshot
-			>;
+			const parsed: unknown = JSON.parse(readFileSync(snapshotsPath, "utf8"));
+			if (!parsed || typeof parsed !== "object") return {};
+			const result: Record<string, CodexUsageSnapshot> = {};
+			for (const [accountId, value] of Object.entries(parsed)) {
+				const candidate = value as Partial<CodexUsageSnapshot>;
+				if (
+					typeof candidate?.accountId === "string" &&
+					typeof candidate.capturedAt === "number" &&
+					Array.isArray(candidate.windows)
+				) {
+					result[accountId] = candidate as CodexUsageSnapshot;
+				}
+			}
+			return result;
 		} catch {
 			return {};
 		}
 	}

A Zod schema built from UsageWindow is stronger, because it also rejects malformed window entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 403 - 412, Update readSnapshots to validate the JSON result with the
existing Zod schema derived from UsageWindow before returning it as
Record<string, CodexUsageSnapshot>; reject invalid root values, snapshots, and
window entries and return {} on validation failure. Remove the unsafe direct
cast while preserving the current fallback behavior for unreadable or invalid
snapshot files.

469-497: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Consume the piped stderr, or the login can hang.

stdio sets stderr to "pipe" at Line 479, but nothing reads child.stderr. The pipe buffer is finite. If codex login writes more than the buffer holds, the child blocks on its next write and never exits, so the flow stalls until the 10-minute CODEX_LOGIN_TIMEOUT_MS fires. The rejection message also discards the stderr text that explains the failure. Collect stderr and include it in the error, or set stderr to "ignore".

🐛 Proposed fix
 					stdio: ["ignore", "ignore", "pipe"],
 				});
+				let stderr = "";
+				child.stderr?.setEncoding("utf8");
+				child.stderr?.on("data", (chunk: string) => {
+					stderr = `${stderr}${chunk}`.slice(-4096);
+				});
 				const timeout = setTimeout(() => {
 					child.kill();
 					reject(new Error("Codex login timed out."));
 				}, CODEX_LOGIN_TIMEOUT_MS);
@@
 					} else {
-						reject(new Error("Codex sign-in did not complete."));
+						reject(
+							new Error(
+								`Codex sign-in did not complete.${stderr ? ` ${stderr.trim()}` : ""}`,
+							),
+						);
 					}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 469 - 497, Update the child process handling in the Codex login flow
around createCodexLoginCommand and spawn to consume child.stderr while the
process runs, preventing pipe-buffer blocking; collect the stderr text and
include it in the rejection error when the exit code is nonzero, while
preserving the existing timeout and successful exit behavior.
🧹 Nitpick comments (1)
apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts (1)

431-452: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Read the snapshots file once per accountRows call.

getSnapshot calls readSnapshots, which performs a synchronous readFileSync and a full JSON.parse. accountRows calls it once per profile, so the same file is read and parsed N times on the Electron main thread. Read the map once and index it inside the map callback.

♻️ Proposed fix
 	function accountRows(): Promise<ProviderUsageAccount[]> {
-		return listProfiles().then((profiles) =>
-			profiles.map((profile) => {
-				const snapshot = getSnapshot(profile.identity.accountId);
+		return listProfiles().then((profiles) => {
+			const snapshots = readSnapshots();
+			return profiles.map((profile) => {
+				const snapshot = snapshots[profile.identity.accountId] ?? null;
 				const windows = snapshot ? projectCachedWindows(snapshot) : [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 431 - 452, Update accountRows to load the snapshots map once before
iterating profiles, then index that map by profile.identity.accountId inside the
map callback instead of calling getSnapshot for each profile. Preserve the
existing fallback behavior when an account has no snapshot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts`:
- Around line 418-429: Serialize putSnapshot operations through a single shared
in-flight promise so each invocation reads the latest snapshots after the
previous write completes. Preserve the existing timestamp guard and atomic write
behavior, and ensure the shared promise is updated and cleared correctly even
when a write fails.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx`:
- Around line 44-48: Update the status message span in AccountUsageBlock to
include the same select-text and cursor-text classes used by the fallback
rendering, while preserving its existing truncation and muted styling.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`:
- Around line 246-254: Wrap the “AI usage meter” label text in HighlightText,
passing query={searchQuery}, while preserving the existing label structure and
htmlFor association. Follow the HighlightText usage used by the other settings
items in BehaviorSettings.
- Around line 256-258: Update the Switch controlled by aiUsageIndicatorEnabled
to import and use DEFAULT_SHOW_AI_USAGE_INDICATOR from shared/constants as the
nullish fallback instead of false, preserving the server-default loading state.

---

Duplicate comments:
In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts`:
- Around line 403-412: Update readSnapshots to validate the JSON result with the
existing Zod schema derived from UsageWindow before returning it as
Record<string, CodexUsageSnapshot>; reject invalid root values, snapshots, and
window entries and return {} on validation failure. Remove the unsafe direct
cast while preserving the current fallback behavior for unreadable or invalid
snapshot files.
- Around line 469-497: Update the child process handling in the Codex login flow
around createCodexLoginCommand and spawn to consume child.stderr while the
process runs, preventing pipe-buffer blocking; collect the stderr text and
include it in the rejection error when the exit code is nonzero, while
preserving the existing timeout and successful exit behavior.

---

Nitpick comments:
In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts`:
- Around line 431-452: Update accountRows to load the snapshots map once before
iterating profiles, then index that map by profile.identity.accountId inside the
map callback instead of calling getSnapshot for each profile. Preserve the
existing fallback behavior when an account has no snapshot.
🪄 Autofix

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 Plus

Run ID: 28a6580f-9139-4bb8-9ec6-cd81ef0190e5

📥 Commits

Reviewing files that changed from the base of the PR and between 6fde8dc and 0ea9a2e.

📒 Files selected for processing (35)
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/MetricToggle.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/shared/constants.ts
  • packages/local-db/drizzle/0046_wise_lorna_dane.sql
  • packages/local-db/drizzle/meta/0046_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/MetricToggle.tsx
  • apps/desktop/src/lib/trpc/routers/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/MetricToggle/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.test.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/index.ts
  • apps/desktop/src/main/lib/provider-usage/index.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/ProviderUsageRow.tsx
  • apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.test.ts
  • apps/desktop/src/main/lib/provider-usage/providers/claude.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/AIUsageIndicator.tsx
  • apps/desktop/src/main/lib/provider-usage/providers/codex-app-server.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/usageIndicatorPolicy.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.ts
  • apps/desktop/src/main/lib/provider-usage/providers/codex.test.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/index.ts
  • apps/desktop/src/lib/trpc/routers/provider-usage.schema.ts
  • apps/desktop/src/main/lib/provider-usage/index.ts

Comment on lines +418 to +429
async function putSnapshot(snapshot: CodexUsageSnapshot): Promise<void> {
ensureStore();
const snapshots = readSnapshots();
const existing = snapshots[snapshot.accountId];
if (existing && existing.capturedAt >= snapshot.capturedAt) return;
snapshots[snapshot.accountId] = snapshot;
await writeSensitiveFileAtomically(
snapshotsPath,
JSON.stringify(snapshots, null, 2),
join(storeRoot, `.snapshots-${randomId()}.json`),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize snapshot writes to avoid lost updates.

putSnapshot reads snapshots.json, mutates the object, and writes the full map back. Two concurrent calls for different accounts both read the same base state, so the later write drops the earlier account entry. Multi-account collection can call this concurrently. Chain the operations through a single in-flight promise.

♻️ Proposed fix
+	let snapshotWriteQueue: Promise<void> = Promise.resolve();
+
 	async function putSnapshot(snapshot: CodexUsageSnapshot): Promise<void> {
-		ensureStore();
-		const snapshots = readSnapshots();
-		const existing = snapshots[snapshot.accountId];
-		if (existing && existing.capturedAt >= snapshot.capturedAt) return;
-		snapshots[snapshot.accountId] = snapshot;
-		await writeSensitiveFileAtomically(
-			snapshotsPath,
-			JSON.stringify(snapshots, null, 2),
-			join(storeRoot, `.snapshots-${randomId()}.json`),
-		);
+		snapshotWriteQueue = snapshotWriteQueue.then(async () => {
+			ensureStore();
+			const snapshots = readSnapshots();
+			const existing = snapshots[snapshot.accountId];
+			if (existing && existing.capturedAt >= snapshot.capturedAt) return;
+			snapshots[snapshot.accountId] = snapshot;
+			await writeSensitiveFileAtomically(
+				snapshotsPath,
+				JSON.stringify(snapshots, null, 2),
+				join(storeRoot, `.snapshots-${randomId()}.json`),
+			);
+		});
+		await snapshotWriteQueue;
 	}
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/provider-usage/providers/codex-profiles.ts` around
lines 418 - 429, Serialize putSnapshot operations through a single shared
in-flight promise so each invocation reads the latest snapshots after the
previous write completes. Preserve the existing timestamp guard and atomic write
behavior, and ensure the shared promise is updated and cleared correctly even
when a write fails.

Comment on lines +44 to +48
{account.statusMessage && (
<span className="truncate text-[9px] text-muted-foreground">
{account.statusMessage}
</span>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the status message selectable here too.

account.statusMessage carries provider failure text, for example an unavailable Codex reading. The fallback render at Line 102 includes select-text cursor-text, but this header render does not. Add the same classes so users can copy the message from both places.

♿ Proposed fix
 					{account.statusMessage && (
-						<span className="truncate text-[9px] text-muted-foreground">
+						<span className="truncate text-[9px] text-muted-foreground select-text cursor-text">
 							{account.statusMessage}
 						</span>
 					)}

As per coding guidelines: "Rendered error messages must include select-text cursor-text classes so users can copy them; Sonner toasts are exempt."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{account.statusMessage && (
<span className="truncate text-[9px] text-muted-foreground">
{account.statusMessage}
</span>
)}
{account.statusMessage && (
<span className="truncate text-[9px] text-muted-foreground select-text cursor-text">
{account.statusMessage}
</span>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/AIUsageIndicator/components/ProviderUsageRow/components/AccountUsageBlock/AccountUsageBlock.tsx`
around lines 44 - 48, Update the status message span in AccountUsageBlock to
include the same select-text and cursor-text classes used by the fallback
rendering, while preserving its existing truncation and muted styling.

Source: Coding guidelines

Comment on lines +246 to +254
<Label
htmlFor="ai-usage-indicator"
className="text-sm font-medium"
>
AI usage meter
</Label>
<p className="text-xs text-muted-foreground">
Show remaining Claude and Codex capacity in the top bar
</p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrap the label text in HighlightText.

This setting is registered as searchable in settings-search.ts with the title "AI usage meter". Every other item in this file passes its title through HighlightText with query={searchQuery}. Without it, a search match shows no highlight for this row.

🔍 Proposed fix
 							<Label
 								htmlFor="ai-usage-indicator"
 								className="text-sm font-medium"
 							>
-								AI usage meter
+								<HighlightText text="AI usage meter" query={searchQuery} />
 							</Label>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Label
htmlFor="ai-usage-indicator"
className="text-sm font-medium"
>
AI usage meter
</Label>
<p className="text-xs text-muted-foreground">
Show remaining Claude and Codex capacity in the top bar
</p>
<Label
htmlFor="ai-usage-indicator"
className="text-sm font-medium"
>
<HighlightText text="AI usage meter" query={searchQuery} />
</Label>
<p className="text-xs text-muted-foreground">
Show remaining Claude and Codex capacity in the top bar
</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`
around lines 246 - 254, Wrap the “AI usage meter” label text in HighlightText,
passing query={searchQuery}, while preserving the existing label structure and
htmlFor association. Follow the HighlightText usage used by the other settings
items in BehaviorSettings.

@Kitenite

Copy link
Copy Markdown
Member

Closing: main already ships this. multi-account usage meters shipped in 60c4d27 (#6530) + 1bc9ab6 (#6585). Thanks for the PR; if you think the shipped version misses a case, open an issue and link this.

@Kitenite Kitenite closed this Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants