🪙 feat: Context Gauge UX, Hover Snapshot, Click Breakdown, Currency, Cost-On-By-Default#13739
Conversation
Flip interface.contextCost to default-on (schema default true, resolved per-field
in loadDefaultInterface so it applies unless an admin explicitly sets false).
Add interface.currency { code, rate }: an ISO-4217 code and a static USD→local
multiplier so non-USD communities (EUR, JPY, CNY, BRL, ZAR, …) can show costs in
their currency. Inner fields are required (no nested defaults) to keep zod
input/output identical; loadDefaultInterface passes it through. Display-only —
model prices stay USD server-side.
formatCost(usd, currency?) applies the static rate (usd × rate) and formats via a cached Intl.NumberFormat keyed by currency code — locale-correct symbol and per-currency decimals, falling back to USD on a malformed code. The USD default (code USD, rate 1) is byte-identical to the prior output.
Replace the hover-only HoverCard with: a compact hover snapshot tooltip
("Context 341.7k / 1.0M (34%)" + cost when enabled) via the existing Tooltip
primitive, and a click-opened Ariakit popover for the full breakdown that
dismisses on outside-click/Escape/blur. Gate visibility on usedTokens > 0 so a
fresh, message-less chat shows nothing, with an animate-in fade as the first
tokens land. Thread the display currency into the breakdown + snapshot.
Switch the breakdown specs from hover to click, and add a test that the gauge is absent on a new chat, surfaces the snapshot tooltip on hover, opens the breakdown on click, and dismisses on Escape and outside-click.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c183b988c8
ℹ️ 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".
| } | ||
|
|
||
| export function formatCost(usd: number, currency: CurrencyConfig = DEFAULT_CURRENCY): string { | ||
| const amount = usd * currency.rate; |
There was a problem hiding this comment.
Avoid converting amounts when the currency fallback is USD
When interface.currency.code is malformed but rate is configured, amount is converted with the local rate before currencyFormatter falls back to a USD formatter, so a typo like { code: 'EURO', rate: 0.92 } displays $9.20 for a $10 cost. That presents a non-USD amount with a USD symbol; either validate/reject the code before startup or fall back both the code and rate to USD.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 90874c3. An unsupported/malformed code now falls back to USD and rate 1 (resolved once up front), so { code: "EURO", rate: 0.92 } renders $10.00 for a $10 cost rather than a converted $9.20 under the wrong symbol. Test added.
| portal | ||
| unmountOnHide | ||
| aria-label={localize('com_ui_context_usage')} | ||
| className="z-50 rounded-xl border border-border-medium bg-surface-secondary p-3 shadow-lg focus:outline-none" |
There was a problem hiding this comment.
Keep the click popover above the hover tooltip
After the intended hover-then-click flow, the snapshot tooltip can remain mounted while the breakdown opens; the shared tooltip CSS uses z-index: 150, but this popover is only z-50, and both are positioned above the same gauge. In that scenario the compact tooltip sits on top of the breakdown, so either hide the tooltip while the popover is open or render the popover at a higher stacking level.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 90874c3 — raised the breakdown popover to z-[200] so it always sits above the z-150 hover tooltip on the brief overlap. (The Ariakit tooltip also dismisses on press/blur, but the stacking guarantee covers the hover-then-click window.)
| minimumFractionDigits: minDigits, | ||
| maximumFractionDigits: maxDigits, |
There was a problem hiding this comment.
Preserve zero-decimal currency formatting
When a deployment configures a valid zero-decimal currency such as JPY, forcing these fraction digits overrides Intl's currency defaults: formatCost(5, { code: 'JPY', rate: 1 }) renders ¥5.00, and sub-unit values render with four decimals. That makes costs in currencies without minor units look like they have cents; derive the fraction digits from the selected currency or only apply the extra precision where the currency supports it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 90874c3. Fraction digits now derive from the currency's own Intl defaults: JPY renders ¥5 (not ¥5.00), and the extra sub-unit precision is applied only to currencies that have minor units. USD output is unchanged. Test added.
| } | ||
|
|
||
| export function formatCost(usd: number, currency: CurrencyConfig = DEFAULT_CURRENCY): string { | ||
| const amount = usd * currency.rate; |
There was a problem hiding this comment.
Guard against incomplete currency overrides
If a DB config override is applied incrementally, for example setting interface.currency.code before interface.currency.rate, this multiplication receives undefined and the UI formats the result as values like €NaN for every cost display. YAML config is schema-checked, but the admin field patch path stores dot-path overrides directly, so formatCost should defensively fall back to the default rate unless currency.rate is a finite number.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 90874c3. formatCost now treats a non-finite or non-positive rate as 1, so a partial override (code set before rate) renders €10.00 instead of €NaN. Test added covering both undefined and NaN rates.
Address Codex review on the currency display:
- Unsupported currency code now falls back to USD AND rate 1, so a typo like
{ code: 'EURO', rate: 0.92 } no longer shows a converted amount under a $
symbol (was $9.20 for a $10 cost; now $10.00).
- A non-finite/negative rate (e.g. a partial admin override that set code before
rate) falls back to rate 1, so a cost never renders as NaN.
- Fraction digits derive from the currency's own defaults, so zero-decimal
currencies (JPY) render ¥5, not ¥5.00, and extra sub-unit precision applies
only to currencies that have minor units. USD output is unchanged.
- Raise the click breakdown popover to z-[200] so it always sits above the
z-150 hover tooltip when both briefly coexist.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90874c382b
ℹ️ 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".
| new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).resolvedOptions() | ||
| .maximumFractionDigits ?? 2; |
There was a problem hiding this comment.
Validate currency codes before trusting Intl
When an admin mistypes a three-letter code such as EUU or configures a common non-ISO code like RMB, Intl.NumberFormat does not throw; it resolves fraction digits and formats with that literal code. Because base is non-null, formatCost then applies the USD→local rate instead of falling back to USD, so users can see a converted amount under an invalid currency label despite the intended fallback. Please validate against supported ISO-4217 codes, or otherwise reject unsupported codes, before applying the rate.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in the latest commit. Now validating against Intl.supportedValuesOf("currency") (the ISO-4217 set) rather than relying on Intl.NumberFormat to throw — so a well-formed-but-bogus code like EUU or a non-ISO RMB is rejected and falls back to USD and rate 1, never a converted amount under a bad label. Tests added for both.
| /** Sub-unit precision only for currencies that have minor units, so | ||
| * zero-decimal currencies (e.g. JPY) keep their convention. */ | ||
| const precise = base > 0 ? base + 2 : 0; | ||
| const smallest = base > 0 ? 0.01 : 1; |
There was a problem hiding this comment.
Use the currency’s minor unit for tiny amounts
For currencies with three fractional digits such as KWD, BHD, or JOD, hard-coding smallest to 0.01 means small converted costs are rounded up to <KWD 0.010 instead of using the actual minor unit (0.001) or preserving values like 0.005. This contradicts the currency-aware formatting path for non-USD communities and overstates the threshold for any configured three-decimal currency; derive this from base instead of assuming cents for every fractional currency.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in the latest commit. The tiny-amount threshold now derives from the currency's minor unit (10^-fractionDigits): 0.001 for KWD/BHD/JOD, 0.01 for 2-decimal currencies, 1 for zero-decimal — no more hard-coded 0.01. Sub-unit precision also trims to each currency's own scale. Test added covering KWD at 0.005 and 0.0005.
Address Codex review on currency formatting:
- Intl.NumberFormat accepts any well-formed 3-letter code (EUU, RMB) without
throwing, so the previous construct-based check missed typos/non-ISO codes and
applied the rate under a bogus label. Validate against Intl.supportedValuesOf
('currency') (the ISO-4217 set); unsupported codes fall back to USD + rate 1.
Codes are normalized to upper-case; graceful fallback if the runtime lacks
supportedValuesOf.
- The tiny-amount threshold now derives from the currency's minor unit
(10^-fractionDigits): 0.01 for 2-decimal, 0.001 for 3-decimal (KWD/BHD/JOD),
1 for zero-decimal — instead of a hard-coded 0.01. Sub-unit precision trims to
each currency's own scale. USD output unchanged.
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
…Cost-On-By-Default (danny-avila#13739) * 🪙 feat: Default Context Cost On + Configurable Display Currency Flip interface.contextCost to default-on (schema default true, resolved per-field in loadDefaultInterface so it applies unless an admin explicitly sets false). Add interface.currency { code, rate }: an ISO-4217 code and a static USD→local multiplier so non-USD communities (EUR, JPY, CNY, BRL, ZAR, …) can show costs in their currency. Inner fields are required (no nested defaults) to keep zod input/output identical; loadDefaultInterface passes it through. Display-only — model prices stay USD server-side. * 🪙 feat: Currency-Aware Context Cost Formatting formatCost(usd, currency?) applies the static rate (usd × rate) and formats via a cached Intl.NumberFormat keyed by currency code — locale-correct symbol and per-currency decimals, falling back to USD on a malformed code. The USD default (code USD, rate 1) is byte-identical to the prior output. * 💄 feat: Gauge Hover Snapshot, Click-to-Open Breakdown, Hide Until Data Replace the hover-only HoverCard with: a compact hover snapshot tooltip ("Context 341.7k / 1.0M (34%)" + cost when enabled) via the existing Tooltip primitive, and a click-opened Ariakit popover for the full breakdown that dismisses on outside-click/Escape/blur. Gate visibility on usedTokens > 0 so a fresh, message-less chat shows nothing, with an animate-in fade as the first tokens land. Thread the display currency into the breakdown + snapshot. * 🧪 test: Gauge Interaction + Visibility E2E Switch the breakdown specs from hover to click, and add a test that the gauge is absent on a new chat, surfaces the snapshot tooltip on hover, opens the breakdown on click, and dismisses on Escape and outside-click. * 🪙 fix: Harden Currency Resolution + Layer Breakdown Above Tooltip Address Codex review on the currency display: - Unsupported currency code now falls back to USD AND rate 1, so a typo like { code: 'EURO', rate: 0.92 } no longer shows a converted amount under a $ symbol (was $9.20 for a $10 cost; now $10.00). - A non-finite/negative rate (e.g. a partial admin override that set code before rate) falls back to rate 1, so a cost never renders as NaN. - Fraction digits derive from the currency's own defaults, so zero-decimal currencies (JPY) render ¥5, not ¥5.00, and extra sub-unit precision applies only to currencies that have minor units. USD output is unchanged. - Raise the click breakdown popover to z-[200] so it always sits above the z-150 hover tooltip when both briefly coexist. * 🪙 fix: Validate ISO-4217 Codes + Derive Tiny Threshold from Minor Unit Address Codex review on currency formatting: - Intl.NumberFormat accepts any well-formed 3-letter code (EUU, RMB) without throwing, so the previous construct-based check missed typos/non-ISO codes and applied the rate under a bogus label. Validate against Intl.supportedValuesOf ('currency') (the ISO-4217 set); unsupported codes fall back to USD + rate 1. Codes are normalized to upper-case; graceful fallback if the runtime lacks supportedValuesOf. - The tiny-amount threshold now derives from the currency's minor unit (10^-fractionDigits): 0.01 for 2-decimal, 0.001 for 3-decimal (KWD/BHD/JOD), 1 for zero-decimal — instead of a hard-coded 0.01. Sub-unit precision trims to each currency's own scale. USD output unchanged.
…Cost-On-By-Default (danny-avila#13739) * 🪙 feat: Default Context Cost On + Configurable Display Currency Flip interface.contextCost to default-on (schema default true, resolved per-field in loadDefaultInterface so it applies unless an admin explicitly sets false). Add interface.currency { code, rate }: an ISO-4217 code and a static USD→local multiplier so non-USD communities (EUR, JPY, CNY, BRL, ZAR, …) can show costs in their currency. Inner fields are required (no nested defaults) to keep zod input/output identical; loadDefaultInterface passes it through. Display-only — model prices stay USD server-side. * 🪙 feat: Currency-Aware Context Cost Formatting formatCost(usd, currency?) applies the static rate (usd × rate) and formats via a cached Intl.NumberFormat keyed by currency code — locale-correct symbol and per-currency decimals, falling back to USD on a malformed code. The USD default (code USD, rate 1) is byte-identical to the prior output. * 💄 feat: Gauge Hover Snapshot, Click-to-Open Breakdown, Hide Until Data Replace the hover-only HoverCard with: a compact hover snapshot tooltip ("Context 341.7k / 1.0M (34%)" + cost when enabled) via the existing Tooltip primitive, and a click-opened Ariakit popover for the full breakdown that dismisses on outside-click/Escape/blur. Gate visibility on usedTokens > 0 so a fresh, message-less chat shows nothing, with an animate-in fade as the first tokens land. Thread the display currency into the breakdown + snapshot. * 🧪 test: Gauge Interaction + Visibility E2E Switch the breakdown specs from hover to click, and add a test that the gauge is absent on a new chat, surfaces the snapshot tooltip on hover, opens the breakdown on click, and dismisses on Escape and outside-click. * 🪙 fix: Harden Currency Resolution + Layer Breakdown Above Tooltip Address Codex review on the currency display: - Unsupported currency code now falls back to USD AND rate 1, so a typo like { code: 'EURO', rate: 0.92 } no longer shows a converted amount under a $ symbol (was $9.20 for a $10 cost; now $10.00). - A non-finite/negative rate (e.g. a partial admin override that set code before rate) falls back to rate 1, so a cost never renders as NaN. - Fraction digits derive from the currency's own defaults, so zero-decimal currencies (JPY) render ¥5, not ¥5.00, and extra sub-unit precision applies only to currencies that have minor units. USD output is unchanged. - Raise the click breakdown popover to z-[200] so it always sits above the z-150 hover tooltip when both briefly coexist. * 🪙 fix: Validate ISO-4217 Codes + Derive Tiny Threshold from Minor Unit Address Codex review on currency formatting: - Intl.NumberFormat accepts any well-formed 3-letter code (EUU, RMB) without throwing, so the previous construct-based check missed typos/non-ISO codes and applied the rate under a bogus label. Validate against Intl.supportedValuesOf ('currency') (the ISO-4217 set); unsupported codes fall back to USD + rate 1. Codes are normalized to upper-case; graceful fallback if the runtime lacks supportedValuesOf. - The tiny-amount threshold now derives from the currency's minor unit (10^-fractionDigits): 0.01 for 2-decimal, 0.001 for 3-decimal (KWD/BHD/JOD), 1 for zero-decimal — instead of a hard-coded 0.01. Sub-unit precision trims to each currency's own scale. USD output unchanged.
Follow-up UX pass on the context-usage gauge (builds on #13670 / #13734, now on
dev). Inspired by Claude Code's status indicator.What changed
Hover → snapshot, click → breakdown. Hovering the gauge now shows a compact one-line snapshot (
Context 341.7k / 1.0M (34%), plus branch cost when enabled) via the existingTooltipprimitive. The full breakdown no longer opens on hover — it opens on click (Ariakit popover) and dismisses on outside-click / Escape / focus-away, standard popover behavior.Hidden until there's data. The gauge is no longer rendered on a fresh, message-less chat (gated on
usedTokens > 0); it eases into view (animate-infade) once the first tokens land.contextCoston by default.interface.contextCostnow defaults totrue(resolved per-field inloadDefaultInterface, so it applies unless an admin explicitly setsfalse). Pricing exposure + cost display follow.Configurable display currency (bare-minimum, accurate). New
interface.currency: { code, rate }— an ISO-4217 code and a static USD→local multiplier — so non-USD communities (EUR, JPY, CNY, BRL, ZAR, …) can show costs in their currency:formatCostappliesusd × rateand formats via a cached, locale-awareIntl.NumberFormat(correct symbol/decimals per currency; falls back to USD on a bad code). Display-only — model prices stay USD server-side. Default (USD, rate 1) is byte-identical to before.Testing
formatCostcurrency/rate/fallback (tokens.spec);loadDefaultInterfacecontextCost-default + currency pass-through (interface.spec, 13 ✓);packages/apitokenConfig + usage gating (79 ✓).usage.specupdated hover→click; new test asserts gauge absent on a new chat, snapshot tooltip on hover, breakdown on click, and dismissal on Escape + outside-click (5 ✓).tsc, eslint, import-sort all clean.Display-only currency; no server-side pricing/FX changes.