Skip to content

feat: replace the usage share card with a fixed-format report - #552

Merged
wibus-wee merged 14 commits into
mainfrom
feat/usage-share-card
Sep 10, 2026
Merged

feat: replace the usage share card with a fixed-format report#552
wibus-wee merged 14 commits into
mainfrom
feat/usage-share-card

Conversation

@wibus-wee

@wibus-wee wibus-wee commented Sep 9, 2026

Copy link
Copy Markdown
Member

Problem / pressure

The usage share card was a canvas "cinema ticket": bespoke rendering with foil
palettes, a VHS filter, an isometric skyline, a WebM encoder and its own webfont
loader, shipped dark behind SHOW_SHARE_CARD = false. Its failure was a product
one, not a technical one — it treated a recurring, comparable record as a novelty
object, so no two cards looked alike and none read at feed thumbnail size.

Summary

Delete that renderer and add a fixed-format React card in its place.

The two share surfaces have opposite jobs, so they get opposite configuration
models. The session card is an editor with nine appearance knobs because its
content has no fixed shape; the usage card is a generator with five, because
its content does — the fewer the choices, the more two months' cards can be read
against each other.

  • Period is the range the Usage screen is already showing, and the headline is
    that range's own timeline total, so the page and the image cannot disagree.
    Every number is derived once in usage-share-stats.ts.
  • Heatmap is always the past 53 weeks with month ticks; the shared range's
    window is lit inside a dimmed year. All-time lights everything.
  • Privacy defaults follow the data, not the gesture. Sharing activity does not
    imply sharing spend, so USD is opt-in. Member identification is a second opt-in,
    offered only when the range has more than one contributor, and carries display
    name and avatar only — computeUsageShareMemberSlices never reads the emails the
    timeline also holds, and a test asserts it.
  • One capture pipeline for both cards. chat-share-image-export.ts became
    share-image-export.ts; chat-share-theme-scope.ts became
    share-theme-scope.ts. Duplicating ~100 lines of snapdom, font readiness and
    Electron bridge handling into a second module was the alternative, and was
    rejected.
  • The entry sits beside the range selector behind an opt-in shareCard prop with a
    lazy dialog, because the public landing reuses StatsSettingsView and must
    neither offer an action it cannot perform nor pull snapdom/qrcode into its bundle.

Net: -3346 / +2303 lines.

Before / after

Before: 1446 lines of canvas plus a shader and a ticket-cutting Storybook, all
unreachable. After: a card composed from design-system tokens, with Storybook
coverage for both formats, both subjects, the hourly range, the cost opt-in and
the unframed variant.

Test plan

  • pnpm check:quick passes: oxlint (0 errors), i18n key parity, Code Collab
    import guard, platform boundary guard, public repository boundary.
  • pnpm --filter @lody/components exec tsgo --noEmit passes.
  • pnpm run docs check reports no errors; pnpm format clean.
  • New tests/usage-share-stats.test.ts (7 cases) covers window-scoped streaks and
    averages, the interval/day switch, the all-time lighting rule, the no-timeline
    fallback, slice ranking and remainder folding, and the email exclusion — all on
    synthetic fixtures with fixed timestamps.
  • tests/share-image-export.test.ts (renamed with its module, 9 cases) still
    covers browser download cleanup, native save cancellation/failure and invalid
    captures.
  • Every card state was rendered in Storybook and inspected; both formats are
    measured to have zero content overflow, and PNG export was exercised end to end.

The full @lody/components suite was run: 3312 of 3313 tests pass under
NODE_ENV=development pnpm --filter @lody/components test. React 19 exports act
only from its development build, so without that variable most files fail with
(0, act) is not a function — that is a missing env var, not a broken worktree.

The one failure is appearance-settings.test.tsx > lets the user pick System in the theme selector. It passes in isolation and fails only under full-suite load; the
file is not touched by this PR and imports none of the modules it adds or renames.
Flagging it rather than claiming it is definitely unrelated.

Context handoff

Instructions for reviewing agents

Focus on the three invariants recorded in
packages/components/src/components/settings/AGENTS.md:

  1. Numbers must not diverge. The card's headline is the timeline total for the
    page's range. Check computeUsageShareStats for the day/interval split and the
    total lighting rule, and that nothing recomputes a total locally.
  2. Privacy defaults. Confirm no path puts USD or a member email on the card
    without the explicit toggle, and that the member mode really is gated on more
    than one contributor.
  3. Layout cannot silently fail. ASPECT_SIZE is the whole exported image
    including the backdrop, so a framed card is 48px shorter — the layout is sized
    for the framed case and every band but the headline is shrink-0 so a card that
    does not fit overflows visibly instead of eating its padding. An earlier revision
    got this wrong and flex quietly compressed the bottom padding to 1px.

Worth challenging: keeping the dates in the hero rather than the footer; the
decision to size both canvases up instead of cutting content; and whether the
shared-pipeline rename is worth the churn in the session card's files.

Evidence gaps: no automated screenshot acceptance, no run against a live
workspace, and rasterization is mocked in the export tests, so pixel fidelity is
unproven.

Authoring context

Requested by the repository owner to delete the unused ticket card and design a
standard share card for workspace usage, informed by — but deliberately not
identical to — the session share card, since the two user journeys differ.
Spec: specs/usage-share-image.md (draft). Decision record:
.agents/notes/implemented/feature/2026-09-09-usage-share-image.md (+ .zh.md).
Mobile (MobileStatsSettings) is intentionally out of scope: it would share via
@capacitor/share rather than a dialog, which is a different interaction.

The previous usage share card was a canvas "cinema ticket" — bespoke rendering
with foil palettes, a VHS filter, an isometric skyline, a WebM encoder and its
own webfont loader — shipped dark behind SHOW_SHARE_CARD = false. It treated a
recurring, comparable record as a novelty object, so no two cards looked alike
and none read at feed thumbnail size.

Delete it and add a fixed-format React card. The session share card is an editor
with nine appearance knobs because its content has no fixed shape; the usage
card is a generator with five because its content does. Both now share one
capture pipeline (lib/share-image-export.ts) and one theme pinning
(components/share-theme-scope.ts).

The card's period is the range the Usage screen is showing and its headline is
that range's own timeline total, so page and image cannot disagree. Cost and
member identification are each opt-in; member slices carry display name and
avatar only, never an email.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 68cdb9fca3

ℹ️ 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 +91 to +95
const elapsed = calendar.cells.filter((cell: UsageCalendarCell) => !cell.isFuture);
const inRange = timeline
? elapsed.filter(
(cell) => cell.dayStartMs >= timeline.startMs && cell.dayStartMs <= timeline.endMs
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope all-time statistics to the all-time period

When the total range spans more than 53 weeks, window still contains only the 53-week calendar, while totalTokens comes from the all-time timeline. The exported card consequently divides an all-time total by at most 371 days for its “daily average” and reports active days, streak, and peak from only the latest year, mixing incompatible periods in a published report. Derive these statistics from data covering the timeline's full period, or avoid presenting them for all-time.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

<RangeSelector range={range} onRangeChange={onRangeChange} />
<div className="flex items-center gap-1.5">
<RangeSelector range={range} onRangeChange={onRangeChange} />
{shareCard && usageCalendar ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for the selected timeline before enabling share

When the independent calendar query resolves before the selected range's timeline, this condition enables Share using only usageCalendar. Opening it then exercises computeUsageShareStats's no-timeline fallback, so a card labeled “Last 24 hours,” “Last 7 days,” or “Last 30 days” can contain the full 53-week calendar total and period instead of the page's still-loading KPI. Require the active usageTimeline before exposing or enabling this action.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T14:20:52.837270Z d983803 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Ghosting the product icon at low opacity kept it legible as a logo, so the card
read as carrying the mark twice — once in the brand row and once parked in a
corner. Treat it as atmosphere instead: brightness-0 flattens the artwork to a
monochrome silhouette (alpha survives, colour does not), inverted on a dark card
so the shadow is light rather than invisible, then scaled well past the card and
clipped by its edge so only a fragment of the bell intrudes from the right.

It now lives on the card root with every band positioned above it, so it cannot
displace a number or change where anything sits.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 8a1230040f

ℹ️ 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 +452 to +454
const periodDates = `${dayFormat.format(new Date(stats.periodMs.fromMs))} – ${dayFormat.format(
new Date(stats.periodMs.toMs)
)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Format the timeline's last covered day

When endMs is the timeline's exclusive boundary, the portrait export formats that boundary directly, so a range ending at midnight is labeled one day late. The existing UsageRangePanel accounts for this by formatting timeline.endMs - DAY_MS; without equivalent handling here, a seven-day card through September 10 prints September 11 and its published absolute period disagrees with the selected range. Format the last covered instant or bucket instead.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

Running the silhouette down the full height put a shadow behind the heatmap and
the model legend, which are the densest bands on the card and did not want one.
Its job is the void beside the headline number, so anchor it to the top-right
corner, clipped by the card's top and right edges, ending above the rule under
the stat row.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 07ae10ee17

ℹ️ 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 +478 to +482
const hero = wide ? (
<div className="flex min-w-0 items-baseline gap-2">
{heroValue}
{heroUnits}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include absolute dates in the wide report

When the user selects the wide format, this branch renders only the total and units; periodDates is rendered exclusively by the portrait branch. The exported 16:9 card therefore says only “Last 30 days” (or another relative label), which no longer identifies its period after the image is shared later. Render the absolute span in the wide layout as well.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

</div>
<svg viewBox={`0 0 ${width} ${height}`} className="block h-auto w-full" role="presentation">
{calendar.cells.map((cell) => {
const inWindow = !lit || (cell.dayStartMs >= lit.fromMs && cell.dayStartMs <= lit.toMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Highlight every day overlapped by hourly ranges

For a day or week timeline that starts after midnight, this predicate treats each calendar day as a single midnight timestamp rather than an interval. The included hourly story, for example, covers 01:00 on the preceding date through 01:00 on the next date, so the date containing 23 of the 24 covered hours fails dayStartMs >= lit.fromMs and is dimmed even though the card claims the range is highlighted. Test overlap against each cell's full UTC-day interval instead.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

Cropping the silhouette against the card's top edge sliced the bell flat and left
a smudge sitting under the range chip. Bleed off the right edge alone instead,
starting below the brand row and ending above the rule under the stat row, so the
mark stays whole and the crop looks chosen rather than accidental.

The wide format now gets no mark at all. It has no void to fill, and the only
place a shadow fits there is over the lit end of the heatmap — the very window the
card exists to highlight.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 7939c0048b

ℹ️ 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".

{ label: t('workspace.usage.skyline.peakDay'), value: compact(stats.peak) },
];
// 16:9 puts the cells on the hero's baseline, where a fourth would not fit.
const trioCells = wide ? allCells.slice(0, 3) : allCells;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve all four headline statistics in wide cards

When the wide format is selected, slicing allCells to three unconditionally removes the peak-day or peak-interval statistic. The usage-card specification requires the same four headline facts at every range, so wide exports are missing a core comparison field that portrait exports retain. Render all four statistics in the wide layout.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

}) {
if (slices.length === 0) return null;
const percent = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 0 });
const rows = compact ? slices.slice(0, 2) : slices;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Render every split row in wide exports

When a range contains three or more models or members and the user selects the wide format, this truncates the legend to two rows even though the percentage bar still renders every slice. The exported report therefore omits contributors represented by the unexplained remaining colors, including the specified third/fourth entries and remainder row. Keep the complete folded split in both formats.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

</span>
{/* Percent alone hides scale: 52% of a quiet week and of a heavy
month are not the same fact, so the row carries both. */}
{compact ? null : (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include absolute token counts in wide split rows

For every wide export with a non-empty model or member split, compact suppresses the absolute token value and leaves only a percentage. This makes the published breakdown lose its scale, contrary to the report contract that each split row carries both tokens and share; for example, a 50% slice no longer indicates whether it represents thousands or millions of tokens. Render the token amount in the wide rows too.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

wibus-wee and others added 3 commits September 10, 2026 05:51
The space next to the headline number kept being filled with the brand mark, and
four treatments of it failed in four different ways — an outline of the wrong
vector, a ghost that read as a second logo, a full-height silhouette behind the
densest bands, a top-cropped one sliced into a smudge. Each fixed the previous
symptom; none fixed the cause, which is that a faded logo is decoration standing
in for content.

Put the range's own profile there instead: one bar per bucket of the period the
headline counts. The card already showed the 53-week year and the totals but never
what these particular days looked like — flat, a late ramp, one burst — which is
the one thing the headline cannot say by itself. Every range has buckets, so it is
always available, and a long period folds into a fixed slot count by summing so
its total stays honest.

The brand mark leaves the card body; the header and footer already carry it.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six attempts went into filling it. Five were the brand mark: an outline stroked
from the wrong vector, a low-opacity ghost that read as a second logo, a
full-height silhouette behind the densest bands, a top-cropped one sliced into a
smudge, and a right-bleeding one that looked deliberate but was still decoration.
The sixth put the range's own bucket profile there, which was at least content and
still one graphic too many beside a card that already carries a year heatmap and a
model split.

Leave it empty. Whitespace beside a headline is a normal thing for a poster to
have. Drop the profile and the `shape` / `resampleShape` derivation with it rather
than shipping data nothing reads, and record the conclusion in AGENTS so the next
pass does not repeat the six.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card drew the 53-week calendar at every range, so a 24h card was a year with
one cell lit — not a comparable record, a wasted band. The Usage screen already
speaks three visual languages, so follow the same split rather than inventing a
fourth: an hour skyline for 24h, a day-by-hour dot grid for 7d, the lit-window
calendar for 30d and all time. A range without an hour-granular series falls back
to the calendar, the one series always present.

This reverses an earlier decision in the note, and the note now says so. The
original reasoning — swapping the block per range doubles the layout surface and
destroys comparability — still holds for the day-denominated ranges and they keep
the calendar. Comparability survives because a card is only ever read against the
same range. The layout surface is contained by giving the graphic one fixed box
every kind fits, so card height does not depend on range.

The week's rows carry weekday and day number: seven days of hour buckets touch
eight calendar days whenever the window does not start at midnight, and the
weekday alone repeats.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 832b2e8897

ℹ️ 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".

const elapsed = calendar.cells.filter((cell: UsageCalendarCell) => !cell.isFuture);
const inRange = timeline
? elapsed.filter(
(cell) => cell.dayStartMs >= timeline.startMs && cell.dayStartMs <= timeline.endMs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include the first partially covered day in range statistics

When a rolling 30-day timeline starts after midnight, this point-in-time predicate excludes the starting calendar date even though much of that day belongs to the selected range. Usage during that covered portion is consequently omitted from active days, streak, and peak, while the headline still uses the complete timeline total; derive these values from the timeline buckets or test calendar-day interval overlap instead.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L33-L36

Useful? React with 👍 / 👎.

function HourAxis({ gap }: { gap: string }) {
return (
<div className={cn('relative h-[12px]', gap)}>
{[0, 6, 12, 18].map((hour) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Label the 24-hour skyline from its first bucket

For a rolling 24-hour timeline that does not begin at 00:00, this fixed axis assigns the wrong hours to every bar. The included HourlyRange story, for example, starts its 24 buckets at 01:00 UTC but labels the first column 00; preserve the bucket timestamps or offset the labels from the timeline start so the exported report does not publish a shifted hourly profile.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L47-L49

Useful? React with 👍 / 👎.

wibus-wee and others added 4 commits September 10, 2026 11:12
Seven days of hour buckets touch eight calendar days whenever the window does not
start at midnight, so a per-row label has to disambiguate the repeated weekday.
Eight rows inside the shared graphic box leave 7px each, which holds no size on
the card's own type scale — so the first attempt reached for an off-scale 8px and
produced exactly the squeezed left column that scale exists to prevent.

Drop the labels. The rows run oldest to newest, the grid now shares the same left
edge as every other band, and the headline already names the span.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured against the session card, the usage footer was already identical to that
card's row variant — 57px, a 33px mark — and carried one field more. What it read
as thin against was the stacked variant at 154px, which a 720px card cannot spend
21% of its height on and the 396px wide card cannot fit at all.

Take the middle for portrait: the session card's identity-plus-sub structure, the
workspace with lody.ai under it, and a full-size code. 73px, paid for out of the
headline band's slack rather than any information band. No EXIF parameter line —
here it would only repeat the bands above. Wide keeps the single row.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Take the session card's canvas footer as well. It is the only placement that gives
the card height instead of taking it: the in-card band disappears and the sign-off
prints on backdrop pixels that were empty frame, so the data gains room. With no
backdrop there is nothing to print on, so it falls back to the in-card footer
exactly as the session card does.

That makes six knobs on a card whose argument is few knobs, which is affordable
because this is a placement rather than a style and it degrades honestly. The other
variants were measured and left out: stacked does not fit, row is what the wide
format already is, minimal drops the workspace name and the code, and exif would
repeat the bands above.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cost was a switch that appended a USD figure beside the token headline, which left
the cells, the graphic and the split still counting tokens. Make it the card's
measure instead: the headline, the four cells, the heatmap's own intensity scale,
the hour and week graphics, and both splits all read whichever unit is chosen, so
no band can disagree with another.

This is also the tighter default. The two units now substitute rather than
accumulate, so a cost card cannot leak a token count alongside the spend, and
tokens stay the default because sharing activity does not imply sharing spend.

The metric threads through usage-share-stats.ts, which is where every number the
card prints is already derived. Renamed the fields it made dishonest —
UsageShareStats.total, UsageShareSlice.value — since neither is token-specific any
more. A deployment reporting no per-model cost simply has no split block on a cost
card, the same as any range without usage; the story fixture now carries real
per-model cost so that block is exercised rather than silently dropped.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: a5bee4d6f2

ℹ️ 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".

fill={
cell.isFuture
? 'hsl(var(--muted-foreground) / 0.05)'
: heatFill(scale.intensity(cell.tokens), inWindow)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Shade calendar cells with the selected metric

When Cost is selected for a month or all-time card, createUsageHeatScale(calendar) is calibrated from each cell's dollar-valued cell.value, but this call feeds it cell.tokens. The resulting token-to-dollar comparison typically clamps every active day to maximum intensity, so the exported heatmap no longer represents relative cost even though every other band is denominated in USD; pass cell.value to the scale instead.

AGENTS.md reference: packages/components/src/components/settings/AGENTS.md:L35-L38

Useful? React with 👍 / 👎.

Tokens were compact everywhere on the card (1.3B, 42M) while money was written
out in full, and a string that grows with its value does not belong in a fixed
layout. Measured on the 16:9 card, the gap between the headline and the stat
cells fell from 208px at 1.3B to 119px at $5,297.05, 10px at $123,456,789.01,
and -18px — an overlap — at ten figures.

formatUsdCompact compacts from a thousand up and keeps smaller amounts exact,
where the cents are the point and the string is short regardless. The gap is now
flat at ~180px whatever the magnitude, and the card has no direct currency
formatting left: one formatter answers for every value it prints.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@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: 6483e002b9

ℹ️ 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 formatUsdCompact(value: number, locale: string | null | undefined): string {
const safeValue = Number.isFinite(value) ? value : 0;
if (Math.abs(safeValue) < USD_COMPACT_FROM) return formatUsdAmount(safeValue, locale);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nonzero sub-cent cost values

When a cost card contains a value below $0.0005—common for a quiet hourly interval or its average—this branch delegates to formatUsdAmount, whose sub-dollar precision is capped at three decimals, so the nonzero value is rendered as $0.00. The exported headline, average, peak, or split can therefore claim there was no spend despite recorded usage; retain sufficient significant precision or use an explicit nonzero compact representation.

Useful? React with 👍 / 👎.

wibus-wee and others added 2 commits September 10, 2026 13:53
Compacting money from a thousand up bounded the width but threw away the thing a
cost card is usually chosen to show: $5.3K hides the digits that are the point.

Shorten in two stages instead. Cents go above a thousand, where on a four-figure
sum they are noise; below a thousand they carry the meaning and stay. Only past a
billion does the figure itself compact, because by then the digits are a wall and
the 16:9 headline shares its row with the stat cells.

$5,297 and $1,234,568 now survive whole. Measured again on the wide card, the
worst surviving case ($999,999,999) leaves a 59px gap where the original
formatting overlapped the cells by 18px.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…umbers

Two faults from the same screenshot.

Preserving a cost figure's digits fixed the headline and pushed the problem into
the stat cells, which have a quarter of its width: they rendered "$42,040…".
An ellipsis on a number is a wrong number, worse than a rounded one, and truncate
had been producing it quietly. Width is a per-slot budget, so formatUsdTight
always compacts above a thousand and serves the cells and the legend, while the
headline keeps its digits through formatUsdCompact.

The same shot showed a headline of "$1.3B" over eight-digit cells, which no real
workspace could produce. metric was a card prop separate from the stats it
described, so a caller could pair one metric's figures with the other's unit —
which a Storybook control did, formatting 1.26 billion tokens as dollars. The
metric now lives inside UsageShareStats, stamped by the function that derives it.

Also scale the large-spend story's calendar with its timeline: the headline comes
from one and the peak from the other, and scaling only one renders a card that
cannot exist.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wibus-wee
wibus-wee merged commit 8686165 into main Sep 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant