Skip to content

feat: show Max/Pro/Team subscription leverage without mislabeling billed providers (#200) - #249

Open
Borisserz wants to merge 2 commits into
chattymin:mainfrom
Borisserz:feat/subscription-leverage
Open

Borisserz wants to merge 2 commits into
chattymin:mainfrom
Borisserz:feat/subscription-leverage

Conversation

@Borisserz

Copy link
Copy Markdown
Contributor

Summary

This is a new PR for #200, not a revival of #224.

#224 was closed because a Claude Max/Pro/Team flag stamped (API-equiv.) on every $ row and on the combined header. Six providers report $. Grok / OpenCode / Hermes $ is a server bill, not a ModelPricing estimate. A Max user who also uses Grok would have seen “(API-equiv.)” on Grok — the opposite of the truth. The close comment also asked for the leverage ratio that motivated the issue ($100/mo against $610 API-equivalent → 6.1×). Caption-only paid seven localization columns with no teaching value.

This PR takes the shape the close comment asked for:

  • Per-provider costIsEstimate (default true = table-priced). Grok, OpenCode, and Hermes set false.

  • No (API-equiv.) caption on today / week / month / per-provider rows, or on the menu bar. Bare $ stays the convention there.

  • Optional Settings field Monthly plan price ($). Default 0 = off. No hardcoded price table.

  • When the Claude credential is Max/Pro/Team and the field is set and estimate $Y > 0, the popover shows one extra line under week/month:

    plan $100.00/mo · API-equiv $610.00 · 6.1×

  • API-key / Free keep today’s presentation. The API-equivalent number is still shown; it is not hidden or zeroed.

Closes nothing until you say so — #200 stays the tracking issue. @shawnla90 offered to take Settings/leverage after #224; I am taking that follow-up here so there is one PR, not two.

Why this is not #224 again

#224 (closed) This PR
One Claude-plan flag UsageProvider.costIsEstimate on each source
(API-equiv.) on every cost row + combined header Qualifier only on the leverage row
No Settings price, no User-entered $X, $Y from estimates, Z× = Y/X
Max+Grok: Grok labeled API-equiv Max+Grok: Grok stays an unlabeled bill; $Y excludes it

Owner scenario from the close comment: Claude Max month $610 estimate + Grok $50 bill + plan $100.

  • Combined month header: $700.00 (unlabeled — still includes the bill).
  • Leverage row: plan $100.00/mo · API-equiv $610.00 · 6.1×.
  • Grok’s own row: bare $, no API-equiv caption.

$Y is not == "claude_code". Gemini (table-priced) and any future estimate source count. Codex still reports cost but zeros totalCost in fetchDaily (pre-existing), so it does not move $Y today.

Type of change

  • New feature
  • Documentation

UI changes

Before After
Popover week/month $ unlabeled for everyone. No plan-price field. Same unlabeled $ on every cost row and in the menu bar. Max/Pro/Team only, and only if Settings has a price > 0: one caption under week/month with plan / API-equiv / multiplier.
Settings → General: no plan price. Optional Monthly plan price ($) field. Hint: set to 0 to hide the row. Default 0.
Menu bar cost mode: $12.3 (compact). Unchanged. No API-equiv, no ×.

Canonical assets/ screenshots are release-time; not updated in this PR.

Design notes (so review does not re-open #224 questions)

  1. Estimate vs bill is a provider fact, not a Claude plan fact. reportsCost still means “draw dollars at all” (Cursor / Copilot / Kiro / Pi / Antigravity stay token-only). costIsEstimate only matters when dollars exist.
  2. Gate is LimitStatus.isFlatRateSubscription (max / pro / team, case-insensitive) ∧ monthlyPlanPrice > 0monthAPIEquivalentCost > 0. Plan lives on LimitStatus so generic totals never branch on providerID.
  3. Menu bar was the open design question on Plan-aware cost: label API-equivalent cost and show subscription leverage #200. The close comment said the bare number is the convention in this category. This PR leaves it unlabeled.
  4. Settings empty field: SwiftUI TextField(value:format:) does not reliably commit empty → 0. The hint says set to 0 to hide, and negatives clamp to 0.

Test plan

  • swift test907 passed, 11 skipped, 0 failures
  • ./scripts/test-gate.sh — logic-core line coverage 91.30% (≥ 75%)
  • Owner Max+Grok example: header $ includes bills; $Y and 6.1× do not (testMonthAPIEquivalentCostExcludesBilledProviders). A future_bill_xyz billed source is in the same fixture so exclusion is the flag, not id == "grok".
  • $Y is not a Claude allow-list: unknown future_tool_xyz estimate of $40 enters $Y and the ratio (testMonthAPIEquivalentCostIncludesUnknownEstimateProvider).
  • Gates: price unset / 0 / Free / no limits / $Y = 0 → no row. Pro / Team / PRO open it (testSubscriptionLeverageGates, testLeverageOpensForProAndTeam).
  • Menu bar has no API / × (testLeverageDoesNotAppearInMenuBar).
  • Popover source does not stamp (API-equiv.) on every cost row (testPopoverLeverageDoesNotRelabelEveryCostRow).
  • In-tree flags: Claude / Gemini / Codex estimate; Grok / OpenCode / Hermes bill (testCostIsEstimateIsPerProviderNotAClaudeFlag).
  • Seven-language interpolation sentinels for the leverage line (LocalizationInterpolationTests).
  • Price persists in UserDefaults; negatives clamp to off.

Checklist

  • swift build and swift test pass locally
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed
  • Tests were added or updated for this change

Related

…led providers (chattymin#200)

chattymin#224 labeled every $ row from a Claude plan flag, so a Max+Grok user saw
"(API-equiv.)" on a real bill. Keep the qualifier on the leverage row only
and compute $Y from per-provider costIsEstimate.

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

Built and ran this same feature on my own machine last week (branch linked in #200), so two notes from that experience:

1. The multiple mixes subscriptions. monthAPIEquivalentCost sums every costIsEstimate provider (Claude, Codex, Gemini, ...) but subscriptionLeverage gates on the Claude plan and divides by one plan price. A Max user who also runs Codex CLI (covered by a ChatGPT subscription, not the Claude plan) gets an inflated 'leverage' number: on my machine, August is ~$2,783 of Claude API-equivalent, and Codex estimates on top would push the displayed multiple past what the Max plan actually delivered. Two clean fixes: scope the numerator to claude_code (the plan the gate reads), or keep the mixed sum and make the row's copy say what it covers. I'd do the former — the hint text says the price is for the Max/Pro/Team row, so users will type their Claude plan price.

2. The price field writes defaults per keystroke. TextField("0", value: $store.monthlyPlanPrice, ...) binds straight to the store, and the didSet writes to UserDefaults on every edit. The scan-roots field from #187 established the draft + commit-on-submit/blur pattern for exactly this reason, and CustomScanRootsTests.testSettingsUsesProviderPickerAndCommitsOnSubmit locks it there ('direct TextField binding to the store writes on every keystroke'). Worth matching that pattern here so the convention holds.

Neither blocks the shape — per-provider costIsEstimate is the right structure and matches the #224 close. Happy to test the branch on my dual-login setup once these land.

…ymin#200)

Gemini/Codex estimates ride other subscriptions, so mixing them into
the Max multiple inflates it. Settings was writing UserDefaults on
every keystroke; match the chattymin#187 draft+commit field.
@Borisserz

Copy link
Copy Markdown
Contributor Author

Landed in c8d7c71.

1. Mixed subscriptions. $Y is now Claude's month estimate only (costIsEstimate && providerID == claude_code). Combined month header still sums everyone unlabeled. Fixture: Claude $610 + Gemini $200 estimate + Grok $50 bill → header $900, leverage 6.1× (not 8.1×). Settings hint now says the field is the Claude Max/Pro/Team price.

2. Per-keystroke defaults. Plan price uses a local draft and commits on submit, blur, and leaving Settings — same pattern as custom scan roots (#187). $store.monthlyPlanPrice is no longer bound to the TextField.

Happy to have you run the dual-login setup against this commit.

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