Skip to content

feat: real-time token cost calculation and subscription ROI dashboard#148

Open
jackrescuer-gif wants to merge 6 commits intomatt1398:mainfrom
jackrescuer-gif:feat/subscription-roi
Open

feat: real-time token cost calculation and subscription ROI dashboard#148
jackrescuer-gif wants to merge 6 commits intomatt1398:mainfrom
jackrescuer-gif:feat/subscription-roi

Conversation

@jackrescuer-gif
Copy link
Copy Markdown

@jackrescuer-gif jackrescuer-gif commented Mar 27, 2026

Closes #147

What this does

Fixes the long-standing costUsd: 0 stub in calculateMetrics() and adds end-to-end cost visibility: from per-token USD calculation through to a dashboard ROI block that compares subscription spend against actual API-equivalent usage.

Changes

feat: add token-to-USD pricing model and fix cost calculation

  • New src/main/utils/pricingModel.ts — pricing table for all Claude model families with getPricingForModel() (prefix-match) and calculateTokenCost(model, input, output, cacheRead, cacheCreate)
  • jsonl.ts — replaces costUsd = 0 stub with per-message cost accumulation using the model field each message reports

feat: add subscription payment tracking to Settings

  • New Billing tab in Settings (CreditCard icon)
  • SubscriptionsSection — add/delete entries grouped by month with monthly total; multiple entries per month supported (e.g. Pro + Max)
  • Config layer: SubscriptionsConfig type, IPC validation for the subscriptions section, persistence via existing config:update

feat: add subscription ROI block to dashboard

  • New RoiBlock component — three stat cards (Paid, API Equivalent, You Saved / Break-even gap) + coverage progress bar
  • New get-usage-stats IPC handler — aggregates token counts and costUsd across all sessions in the current month
  • Empty state with link to Settings > Billing when no subscription is configured

test: add tests for pricingModel and subscriptions config validation

  • pricingModel.test.ts — model lookup, cost calculation, linear scaling
  • configValidation.test.ts — subscriptions section, per-entry validation

Validation checklist

  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm test — please run on your end
  • Manually tested end-to-end: subscriptions save/load, ROI block renders with real session data ($879 API equivalent vs $120 subscription)
  • No new npm dependencies
  • All dates use 'en-US' locale explicitly

Summary by CodeRabbit

  • New Features

    • Billing tab in Settings to add/manage subscription payments (date, plan, amount, note).
    • Dashboard ROI block comparing monthly subscription spend vs. API-equivalent usage with progress and savings.
    • Usage analytics endpoint to fetch monthly usage/cost stats for ROI display.
  • Bug Fixes / Improvements

    • More accurate per-model token cost calculations.
  • Tests

    • Added tests for pricing logic and subscription validation.

Pawel Novak added 5 commits March 27, 2026 22:51
Previously calculateMetrics() always returned costUsd=0 (stub).
This adds a complete pricing table for all Claude model families
(claude-2/instant/3/3.5/3.7/4) with input, output, cache-read
and cache-write rates, and wires it into the metrics calculation
so every session now reports a real estimated USD cost.

- src/main/utils/pricingModel.ts: new module with pricing table
  and helpers getPricingForModel() (prefix-match) + calculateTokenCost()
- src/main/utils/jsonl.ts: replace costUsd=0 stub with per-message
  cost accumulation using the model field reported in each message
Adds a new Billing tab in Settings where users can record their
Claude subscription charges (multiple entries per month supported,
e.g. Pro + Max on different dates). Entries are persisted via the
existing config:update IPC channel with full validation.

- ConfigManager: new SubscriptionsConfig + SubscriptionEntry types,
  defaults, and merge logic
- shared/types: AppConfig and ElectronAPI extended with subscriptions
  and getUsageStats()
- configValidation: subscriptions section with per-entry validation
  (id, ISO date, plan, positive amountUsd, optional note)
- preload: getUsageStats IPC bridge
- SettingsTabs: new Billing tab with CreditCard icon
- SettingsView: render SubscriptionsSection for billing section
- SubscriptionsSection: form UI — add/delete entries, grouped by
  month with monthly total, locale forced to en-US
Shows a month-level cost comparison at the top of the dashboard:
subscription paid vs estimated API-equivalent cost computed from
actual token usage across all sessions in the current month.

Three stat cards: Paid (subscription), API Equivalent, You Saved
(or break-even gap). Progress bar shows API usage as % of sub cost.
Links to Settings > Billing when no subscription is configured.

- sessions.ts: new get-usage-stats IPC handler — scans all projects,
  filters sessions by month, aggregates token counts and costUsd
- RoiBlock.tsx: dashboard component consuming getUsageStats() IPC
  and appConfig.subscriptions; all dates formatted as en-US
- DashboardView.tsx: mount RoiBlock between search bar and projects
- pricingModel.test.ts: covers getPricingForModel (known models,
  prefix matching, opus>haiku ordering, unknown fallback) and
  calculateTokenCost (zero, input-only, output-only, cache tokens,
  output>input cost, linear scaling)
- configValidation.test.ts: adds subscriptions describe block —
  valid empty/single/multi entries, optional note, rejects missing
  id, bad date format, zero/negative amount, non-string note
- pricingModel.ts: Array<T> → T[] (array-type rule)
- notifications.ts: Array<T> → T[] (array-type rule)
- configValidation.ts: remove unnecessary type assertion on amountUsd
- sessions.ts: fix import sort order, merge duplicate @shared/types imports
- jsonl.ts: move ./pricingModel import into relative-imports group
- DashboardView.tsx: fix import sort (logger const was between imports)
- RoiBlock.tsx: use @renderer/types/data for AppConfig (matches store),
  remove unnecessary as-cast on s.appConfig
- SettingsView.tsx: remove unused AppConfig import
- httpClient.ts: import UsageStats at top, remove inline import() type
- SubscriptionsSection.tsx: remove unused api/useStore imports,
  replace Math.random() with crypto.randomUUID(), wrap entries in
  useMemo to stabilise useCallback deps, remove unused y/m vars,
  add htmlFor/id pairs on all form labels (a11y), remove ! assertion
@coderabbitai coderabbitai bot added the feature request New feature or request label Mar 27, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a subscription ROI tracking system, allowing users to compare their subscription costs against equivalent pay-per-token API pricing. Key additions include a Claude API pricing model, backend logic for aggregating monthly usage stats, and frontend components for managing billing entries and displaying ROI metrics on the dashboard. The review identifies duplicate model entries in the pricing table and suggests centralizing subscription type definitions to improve maintainability and reduce duplication between shared types and service configurations.

Comment on lines +52 to +55
[
'claude-3.5-sonnet',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],
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.

medium

This is a duplicate entry for claude-3-5-sonnet. The same entry is already defined on lines 48-51. You can remove this duplicate block to improve maintainability.

Comment on lines +60 to +63
[
'claude-3.5-haiku',
{ inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 },
],
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.

medium

This is a duplicate entry for claude-3-5-haiku. The same entry is already defined on lines 56-59. You can remove this duplicate block.

Comment on lines +319 to +330
subscriptions?: {
entries: {
id: string;
/** ISO date string, e.g. "2026-03-01" */
date: string;
/** Plan label: "Pro", "Max", "Team", etc. */
plan: string;
/** Amount paid in USD */
amountUsd: number;
note?: string;
}[];
};
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.

medium

The type for subscriptions is defined inline here, while SubscriptionEntry and SubscriptionsConfig are also defined and exported in src/main/services/infrastructure/ConfigManager.ts. This creates type duplication, which can lead to maintenance issues.

To centralize these types, you could define SubscriptionEntry and SubscriptionsConfig in this shared types file and export them. Then, AppConfig can use SubscriptionsConfig as suggested.

You would also need to remove the definitions from ConfigManager.ts and import them there instead.

Example of types to add and export from this file:

export interface SubscriptionEntry {
  id: string;
  /** ISO date string, e.g. "2026-03-01" */
  date: string;
  /** Plan label: "Pro", "Max", "Team", etc. */
  plan: string;
  /** Amount paid in USD */
  amountUsd: number;
  note?: string;
}

export interface SubscriptionsConfig {
  entries: SubscriptionEntry[];
}
  subscriptions?: SubscriptionsConfig;

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ca68165-6f34-4365-a392-7c06350f6c22

📥 Commits

Reviewing files that changed from the base of the PR and between c75edf4 and 403c03a.

📒 Files selected for processing (3)
  • src/main/services/infrastructure/ConfigManager.ts
  • src/main/utils/pricingModel.ts
  • src/shared/types/notifications.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/utils/pricingModel.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/services/infrastructure/ConfigManager.ts
  • src/shared/types/notifications.ts

📝 Walkthrough

Walkthrough

Adds per-message token pricing and monthly USD cost calculation, subscription payment tracking in config and settings, an IPC + HTTP surface to aggregate monthly usage stats, and a dashboard ROI block comparing subscription spend to API-equivalent cost.

Changes

Cohort / File(s) Summary
Pricing & Metrics
src/main/utils/pricingModel.ts, src/main/utils/jsonl.ts, test/main/utils/pricingModel.test.ts
New pricing model and cost calculation APIs; calculateMetrics updated to accumulate per-message token costs; tests added for pricing lookup and cost computation.
Subscriptions Config & Validation
src/main/ipc/configValidation.ts, src/main/services/infrastructure/ConfigManager.ts, src/shared/types/notifications.ts, test/main/ipc/configValidation.test.ts
Added SubscriptionEntry/SubscriptionsConfig types, default subscriptions in config manager, and validation for subscriptions update payload with tests for valid/invalid entries.
Usage Stats IPC & API Surface
src/main/ipc/sessions.ts, src/preload/index.ts, src/shared/types/api.ts, src/renderer/api/httpClient.ts
New get-usage-stats IPC handler that aggregates monthly token counts and cost; exposed via preload bridge and renderer HTTP client; UsageStats type added.
Settings UI — Billing / Subscriptions
src/renderer/components/settings/SettingsTabs.tsx, src/renderer/components/settings/SettingsView.tsx, src/renderer/components/settings/sections/SubscriptionsSection.tsx, src/renderer/components/settings/sections/index.ts
Added Billing tab and SubscriptionsSection UI for CRUD of subscription payments, client-side validation, grouping by month, and save/delete flows.
Dashboard ROI Block
src/renderer/components/dashboard/DashboardView.tsx, src/renderer/components/dashboard/RoiBlock.tsx
Inserted ROI widget on dashboard that reads subscription entries, fetches monthly usage stats, displays paid vs API-equivalent cost, and shows coverage progress.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements all three core requirements from issue #147: token pricing model with rates for all Claude families, subscription tracker with validation and persistence, and ROI dashboard block comparing subscription paid vs API-equivalent cost.
Out of Scope Changes check ✅ Passed All changes align with PR objectives. No unrelated modifications detected; pricing model, subscription config, IPC handlers, UI components, and tests directly support the ROI feature.

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


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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
src/preload/index.ts (1)

160-162: Consider defining the IPC channel constant in ipcChannels.ts.

The 'get-usage-stats' channel string is hardcoded here. Per project conventions, IPC channel constants should be defined in src/preload/constants/ipcChannels.ts. However, I note that many existing session/project methods (e.g., 'get-projects', 'get-sessions') follow the same hardcoded pattern, so this is consistent with the current codebase style.

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

In `@src/preload/index.ts` around lines 160 - 162, Replace the hardcoded IPC
channel string in the getUsageStats preload method with the centralized constant
from src/preload/constants/ipcChannels.ts: update the getUsageStats function to
import and use the appropriate constant (e.g., GET_USAGE_STATS or similar)
instead of the literal 'get-usage-stats' and adjust the import list at the top
of src/preload/index.ts to include that constant; ensure the constant name
matches what is exported from ipcChannels.ts and update any other similar
hardcoded usages in this file for consistency.
src/shared/types/notifications.ts (1)

318-330: Type inconsistency: subscriptions is optional here but required in ConfigManager.ts.

Per the relevant code snippets, ConfigManager.ts (line 246) defines AppConfig.subscriptions as required (subscriptions: SubscriptionsConfig), while this shared type marks it as optional (subscriptions?:). Since DEFAULT_CONFIG always initializes subscriptions.entries = [], the config will always have this field at runtime.

This inconsistency can cause:

  1. Unnecessary null checks in renderer code
  2. Potential confusion about the actual contract

Consider aligning the types:

♻️ Proposed fix to make subscriptions required
   /** Subscription payment history for ROI tracking */
-  subscriptions?: {
+  subscriptions: {
     entries: {
       id: string;
       /** ISO date string, e.g. "2026-03-01" */
       date: string;
       /** Plan label: "Pro", "Max", "Team", etc. */
       plan: string;
       /** Amount paid in USD */
       amountUsd: number;
       note?: string;
     }[];
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/types/notifications.ts` around lines 318 - 330, The subscriptions
property in the shared type is declared optional but ConfigManager.ts/ AppConfig
and DEFAULT_CONFIG treat it as required; make the shapes consistent by removing
the optional marker so subscriptions is required (e.g., change subscriptions?:
{...} to subscriptions: {...}) and ensure the exported type name used by
ConfigManager (AppConfig or SubscriptionsConfig) matches this updated required
definition so callers no longer need null checks.
src/renderer/api/httpClient.ts (1)

274-276: Add explicit type parameter to this.get() for type safety.

Other methods in this class explicitly specify the type parameter (e.g., this.get<RepositoryGroup[]>(...)), but this method omits it. While TypeScript infers from the return type annotation, being explicit improves clarity and consistency.

♻️ Proposed fix
   getUsageStats = (year: number, month: number): Promise<UsageStats> =>
-    this.get(`/api/usage-stats?year=${year}&month=${month}`);
+    this.get<UsageStats>(`/api/usage-stats?year=${year}&month=${month}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/api/httpClient.ts` around lines 274 - 276, The getUsageStats
method omits the explicit type parameter on the HTTP helper call; update the
call to this.get by adding the explicit generic type UsageStats (i.e., use
this.get<UsageStats>(...)) so the getUsageStats method (and its return value) is
typed consistently with other methods and improves clarity and type safety.
src/main/ipc/sessions.ts (1)

398-424: Consider caching for large-scale usage.

For users with many sessions, re-parsing all JSONL files on each call could become slow. Since this is called on-demand for dashboard display with monthly scope, the current implementation is acceptable. If performance becomes an issue, consider caching aggregated metrics per session.

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

In `@src/main/ipc/sessions.ts` around lines 398 - 424, The loop in sessions.ts
currently reparses every session JSONL (via parseJsonlFile and calculateMetrics)
on each get-usage-stats call which will be slow at scale; implement a simple
per-session aggregated-metrics cache keyed by project.id+session.id (or a
sidecar file next to getSessionPath output) that stores precomputed metrics
(inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, costUsd,
createdAt) and update it when sessions are created/modified or when missing/old;
change the usage in the get-usage-stats flow to try reading the cached metrics
first and only call parseJsonlFile/calculateMetrics when the cache is absent or
stale, then aggregate cached values into stats and persist the new cache entry
for future calls.
src/renderer/components/dashboard/RoiBlock.tsx (1)

206-222: Verify savings percentage calculation edge case.

The savings percentage at line 218 divides by subPaid. While the component structure ensures subPaid > 0 when this code path executes (via the early return at line 143 and the hasUsage conditional), consider adding a defensive check for clarity.

💡 Optional defensive guard
 subtitle={
   hasUsage
     ? savings >= 0
-      ? `${((savings / subPaid) * 100).toFixed(0)}% return on subscription`
+      ? `${subPaid > 0 ? ((savings / subPaid) * 100).toFixed(0) : 0}% return on subscription`
       : `Need $${(subPaid - apiEquiv).toFixed(2)} more API usage to break even`
     : undefined
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/dashboard/RoiBlock.tsx` around lines 206 - 222, The
savings percent calculation can divide by zero; update the subtitle logic in the
StatCard block (where savings, subPaid and hasUsage are used) to defensively
check subPaid > 0 before computing (savings / subPaid) * 100 and fall back to a
safe value (e.g. '—' or '0%') when subPaid is 0 or falsy; specifically modify
the subtitle expression that currently produces `${((savings / subPaid) *
100).toFixed(0)}% return on subscription` to conditionally compute the
percentage only when subPaid > 0 (keep the hasUsage check) to avoid potential
runtime errors.
src/renderer/components/settings/sections/SubscriptionsSection.tsx (1)

91-106: Consider adding error handling to handleAdd.

Unlike handleDelete (lines 108-120), handleAdd doesn't wrap onSave in try/catch. If the save fails, the form will still close and reset, potentially confusing the user who won't know the entry wasn't saved.

🛡️ Suggested fix with error handling
 const handleAdd = useCallback(async () => {
   if (saving) return;
   if (!amountValid) {
     setAmountError('Enter a valid amount greater than 0');
     return;
   }
   setAmountError(null);
   const next: SubscriptionEntry[] = [
     ...entries,
     { id: newId(), date: form.date, plan: effectivePlan, amountUsd: amountNum, note: form.note.trim() || undefined },
   ].sort((a, b) => a.date.localeCompare(b.date));
-  await onSave(next);
-  setAmountError(null);
-  setForm({ date: todayIso(), plan: 'Pro', customPlan: '', amountUsd: '', note: '' });
-  setShowForm(false);
+  try {
+    await onSave(next);
+    setForm({ date: todayIso(), plan: 'Pro', customPlan: '', amountUsd: '', note: '' });
+    setShowForm(false);
+  } catch (err) {
+    logger.error('Failed to add subscription entry:', err);
+    setAmountError('Failed to save entry');
+  }
 }, [saving, amountValid, entries, form, effectivePlan, amountNum, onSave]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/settings/sections/SubscriptionsSection.tsx` around
lines 91 - 106, handleAdd currently awaits onSave(next) without error handling
so the form resets/close even when save fails; wrap the await onSave(next) call
in a try/catch: in try await onSave(next) then perform the post-save actions
(setForm(...), setShowForm(false), clear errors), and in catch setAmountError to
a user-friendly message (e.g., error.message or a generic "Failed to save
subscription") and do not reset/close the form so the user can retry; follow the
same error-handling pattern used in handleDelete and reference handleAdd,
onSave, setShowForm, setForm, and setAmountError when making the change.
src/main/utils/pricingModel.ts (1)

110-114: The includes fallback may be overly permissive.

The combined startsWith || includes check could match unexpected model strings (e.g., "custom-claude-3-opus-wrapper" would match via includes). If model strings always follow Anthropic's naming convention, this is fine. If third-party wrappers might pass custom model identifiers, consider whether startsWith alone would suffice.

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

In `@src/main/utils/pricingModel.ts` around lines 110 - 114, The current loop over
PRICING_TABLE uses lower.startsWith(prefix) || lower.includes(prefix), which is
too permissive and can match wrapped/custom identifiers like
"custom-claude-3-opus-wrapper"; update the check in the pricing resolution logic
(the loop iterating over PRICING_TABLE and using the lower variable) to use a
stricter match — e.g., drop the includes fallback and rely on
startsWith(prefix), or implement a safer match such as startsWith(prefix) or
matching prefix with common separators (e.g., `lower.startsWith(prefix)` or
`lower.includes('-' + prefix)` or a regex that enforces prefix boundaries) so
only intended Anthropic model identifiers map to PRICING_TABLE entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/utils/pricingModel.ts`:
- Around line 26-88: Update the PRICING_TABLE entries to match current Anthropic
pricing: change the 'claude-opus-4' input/output to 5.0/25.0 and change
'claude-haiku-4' input/output to 1.0/5.0; add new entries for the 4.6 and 4.5
variants (e.g., 'claude-opus-4-6'/'claude-sonnet-4-6' and
'claude-opus-4-5'/'claude-sonnet-4-5') with their correct input/output/cache
rates per the Anthropic docs, and either remove or mark 'claude-2' and
'claude-instant' as legacy (add an inline "legacy" comment and keep or remove
the entries accordingly); ensure cacheWritePerMillion/cacheReadPerMillion values
are updated to match current rates and, if keeping legacy models with cache
rates equal to input by design, add a comment in PRICING_TABLE explaining that
choice.

In `@src/renderer/components/dashboard/DashboardView.tsx`:
- Around line 22-25: The RepositoryGroup type import is misplaced after the
logger const, breaking ESLint import grouping; move the import statement for
RepositoryGroup so it sits with the other imports at the top (above the logger
const) alongside imports like RoiBlock, ensuring all import statements are
grouped and ordered correctly to satisfy ESLint.

---

Nitpick comments:
In `@src/main/ipc/sessions.ts`:
- Around line 398-424: The loop in sessions.ts currently reparses every session
JSONL (via parseJsonlFile and calculateMetrics) on each get-usage-stats call
which will be slow at scale; implement a simple per-session aggregated-metrics
cache keyed by project.id+session.id (or a sidecar file next to getSessionPath
output) that stores precomputed metrics (inputTokens, outputTokens,
cacheReadTokens, cacheCreationTokens, costUsd, createdAt) and update it when
sessions are created/modified or when missing/old; change the usage in the
get-usage-stats flow to try reading the cached metrics first and only call
parseJsonlFile/calculateMetrics when the cache is absent or stale, then
aggregate cached values into stats and persist the new cache entry for future
calls.

In `@src/main/utils/pricingModel.ts`:
- Around line 110-114: The current loop over PRICING_TABLE uses
lower.startsWith(prefix) || lower.includes(prefix), which is too permissive and
can match wrapped/custom identifiers like "custom-claude-3-opus-wrapper"; update
the check in the pricing resolution logic (the loop iterating over PRICING_TABLE
and using the lower variable) to use a stricter match — e.g., drop the includes
fallback and rely on startsWith(prefix), or implement a safer match such as
startsWith(prefix) or matching prefix with common separators (e.g.,
`lower.startsWith(prefix)` or `lower.includes('-' + prefix)` or a regex that
enforces prefix boundaries) so only intended Anthropic model identifiers map to
PRICING_TABLE entries.

In `@src/preload/index.ts`:
- Around line 160-162: Replace the hardcoded IPC channel string in the
getUsageStats preload method with the centralized constant from
src/preload/constants/ipcChannels.ts: update the getUsageStats function to
import and use the appropriate constant (e.g., GET_USAGE_STATS or similar)
instead of the literal 'get-usage-stats' and adjust the import list at the top
of src/preload/index.ts to include that constant; ensure the constant name
matches what is exported from ipcChannels.ts and update any other similar
hardcoded usages in this file for consistency.

In `@src/renderer/api/httpClient.ts`:
- Around line 274-276: The getUsageStats method omits the explicit type
parameter on the HTTP helper call; update the call to this.get by adding the
explicit generic type UsageStats (i.e., use this.get<UsageStats>(...)) so the
getUsageStats method (and its return value) is typed consistently with other
methods and improves clarity and type safety.

In `@src/renderer/components/dashboard/RoiBlock.tsx`:
- Around line 206-222: The savings percent calculation can divide by zero;
update the subtitle logic in the StatCard block (where savings, subPaid and
hasUsage are used) to defensively check subPaid > 0 before computing (savings /
subPaid) * 100 and fall back to a safe value (e.g. '—' or '0%') when subPaid is
0 or falsy; specifically modify the subtitle expression that currently produces
`${((savings / subPaid) * 100).toFixed(0)}% return on subscription` to
conditionally compute the percentage only when subPaid > 0 (keep the hasUsage
check) to avoid potential runtime errors.

In `@src/renderer/components/settings/sections/SubscriptionsSection.tsx`:
- Around line 91-106: handleAdd currently awaits onSave(next) without error
handling so the form resets/close even when save fails; wrap the await
onSave(next) call in a try/catch: in try await onSave(next) then perform the
post-save actions (setForm(...), setShowForm(false), clear errors), and in catch
setAmountError to a user-friendly message (e.g., error.message or a generic
"Failed to save subscription") and do not reset/close the form so the user can
retry; follow the same error-handling pattern used in handleDelete and reference
handleAdd, onSave, setShowForm, setForm, and setAmountError when making the
change.

In `@src/shared/types/notifications.ts`:
- Around line 318-330: The subscriptions property in the shared type is declared
optional but ConfigManager.ts/ AppConfig and DEFAULT_CONFIG treat it as
required; make the shapes consistent by removing the optional marker so
subscriptions is required (e.g., change subscriptions?: {...} to subscriptions:
{...}) and ensure the exported type name used by ConfigManager (AppConfig or
SubscriptionsConfig) matches this updated required definition so callers no
longer need null checks.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e606f96d-377d-4a88-9f98-f72a3e387c6b

📥 Commits

Reviewing files that changed from the base of the PR and between f23e881 and c75edf4.

📒 Files selected for processing (17)
  • src/main/ipc/configValidation.ts
  • src/main/ipc/sessions.ts
  • src/main/services/infrastructure/ConfigManager.ts
  • src/main/utils/jsonl.ts
  • src/main/utils/pricingModel.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/renderer/components/dashboard/DashboardView.tsx
  • src/renderer/components/dashboard/RoiBlock.tsx
  • src/renderer/components/settings/SettingsTabs.tsx
  • src/renderer/components/settings/SettingsView.tsx
  • src/renderer/components/settings/sections/SubscriptionsSection.tsx
  • src/renderer/components/settings/sections/index.ts
  • src/shared/types/api.ts
  • src/shared/types/notifications.ts
  • test/main/ipc/configValidation.test.ts
  • test/main/utils/pricingModel.test.ts

Comment on lines +26 to +88
const PRICING_TABLE: [string, ModelPricing][] = [
// ── Claude 4 ──────────────────────────────────────────────────────────
[
'claude-opus-4',
{ inputPerMillion: 15.0, outputPerMillion: 75.0, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.5 },
],
[
'claude-sonnet-4',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],
[
'claude-haiku-4',
{ inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 },
],

// ── Claude 3.7 ────────────────────────────────────────────────────────
[
'claude-3-7-sonnet',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],

// ── Claude 3.5 ────────────────────────────────────────────────────────
[
'claude-3-5-sonnet',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],
[
'claude-3.5-sonnet',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],
[
'claude-3-5-haiku',
{ inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 },
],
[
'claude-3.5-haiku',
{ inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 },
],

// ── Claude 3 ──────────────────────────────────────────────────────────
[
'claude-3-opus',
{ inputPerMillion: 15.0, outputPerMillion: 75.0, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.5 },
],
[
'claude-3-sonnet',
{ inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 },
],
[
'claude-3-haiku',
{ inputPerMillion: 0.25, outputPerMillion: 1.25, cacheWritePerMillion: 0.3, cacheReadPerMillion: 0.03 },
],

// ── Claude 2 ──────────────────────────────────────────────────────────
[
'claude-2',
{ inputPerMillion: 8.0, outputPerMillion: 24.0, cacheWritePerMillion: 8.0, cacheReadPerMillion: 8.0 },
],
[
'claude-instant',
{ inputPerMillion: 1.63, outputPerMillion: 5.51, cacheWritePerMillion: 1.63, cacheReadPerMillion: 1.63 },
],
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What are the current token pricing rates for Claude API models including Claude 4, Claude 3.5, and Claude 3?

💡 Result:

Current Claude API token pricing rates (per million tokens, USD, as of March 2026) from official Anthropic documentation: Claude 4 models (latest flagship family): - Claude Opus 4.6: Input $5, Output $25 - Claude Sonnet 4.6: Input $3, Output $15 - Claude Opus 4.5: Input $5, Output $25 - Claude Sonnet 4.5: Input $3, Output $15 - Claude Haiku 4.5: Input $1, Output $5 Claude 3.5 models: - Claude Haiku 3.5: Input $0.80, Output $4 Claude 3 models (legacy/deprecated): - Claude Opus 3: Input $15, Output $75 - Claude Sonnet 3.7: Input $3, Output $15 (deprecated) - Claude Haiku 3: Input $0.25, Output $1.25 Notes: Prices are for base input/output tokens. Prompt caching, batch processing (50% discount), and long-context may modify rates. Claude 4 represents the current production models; Claude 3/3.5 are older generations still available but some deprecated. No specific "Claude 3.5 Sonnet" or "Claude 3 Opus/Sonnet" listed separately in current docs—likely rolled into 4.x or legacy 3.7/3.

Citations:


Update PRICING_TABLE to reflect current Anthropic API pricing (as of March 2026).

The hardcoded pricing values are outdated and will cause inaccurate cost calculations:

  • claude-opus-4: Input/Output should be $5/$25 (not $15/$75)
  • claude-haiku-4: Input/Output should be $1/$5 (not $0.8/$4)
  • Missing newer model variants: Claude Opus/Sonnet 4.6 and 4.5 entries should be added
  • Claude 2 and Instant are deprecated and no longer in current production pricing; consider removing or clearly marking as legacy

Reference current pricing from Anthropic documentation.

Regarding Claude 2/Instant cache rates matching input rates—if intentionally supporting legacy models without caching, add an inline comment explaining this is by design.

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

In `@src/main/utils/pricingModel.ts` around lines 26 - 88, Update the
PRICING_TABLE entries to match current Anthropic pricing: change the
'claude-opus-4' input/output to 5.0/25.0 and change 'claude-haiku-4'
input/output to 1.0/5.0; add new entries for the 4.6 and 4.5 variants (e.g.,
'claude-opus-4-6'/'claude-sonnet-4-6' and 'claude-opus-4-5'/'claude-sonnet-4-5')
with their correct input/output/cache rates per the Anthropic docs, and either
remove or mark 'claude-2' and 'claude-instant' as legacy (add an inline "legacy"
comment and keep or remove the entries accordingly); ensure
cacheWritePerMillion/cacheReadPerMillion values are updated to match current
rates and, if keeping legacy models with cache rates equal to input by design,
add a comment in PRICING_TABLE explaining that choice.

Comment on lines 22 to +25
import type { RepositoryGroup } from '@renderer/types/data';

import { RoiBlock } from './RoiBlock';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Import ordering issue flagged by ESLint.

The type import on line 22 appears after the logger const declaration (line 20), which breaks the import grouping. Move the RepositoryGroup type import to join the other imports before line 20.

🔧 Proposed fix to reorder imports
 import { formatDistanceToNow } from 'date-fns';
 import { Command, FolderGit2, FolderOpen, GitBranch, Search, Settings } from 'lucide-react';
 import { useShallow } from 'zustand/react/shallow';

+import type { RepositoryGroup } from '@renderer/types/data';
+
+import { RoiBlock } from './RoiBlock';
+
 const logger = createLogger('Component:DashboardView');

-import type { RepositoryGroup } from '@renderer/types/data';
-
-import { RoiBlock } from './RoiBlock';
🧰 Tools
🪛 ESLint

[error] 22-24: Run autofix to sort these imports!

(simple-import-sort/imports)

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

In `@src/renderer/components/dashboard/DashboardView.tsx` around lines 22 - 25,
The RepositoryGroup type import is misplaced after the logger const, breaking
ESLint import grouping; move the import statement for RepositoryGroup so it sits
with the other imports at the top (above the logger const) alongside imports
like RoiBlock, ensuring all import statements are grouped and ordered correctly
to satisfy ESLint.

- pricingModel.ts: remove duplicate claude-3.5-sonnet and
  claude-3.5-haiku entries (dot-notation variants were identical
  to the already-present dash-notation entries)
- notifications.ts: extract SubscriptionEntry + SubscriptionsConfig
  as named exported interfaces; AppConfig.subscriptions now uses
  SubscriptionsConfig instead of an inline anonymous type
- ConfigManager.ts: remove now-redundant local interface definitions,
  re-export SubscriptionEntry/SubscriptionsConfig from @shared/types
  for backwards compatibility with existing imports
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: real-time token cost calculation + subscription ROI dashboard

1 participant