feat: replace the usage share card with a fixed-format report - #552
feat: replace the usage share card with a fixed-format report#552wibus-wee wants to merge 14 commits into
Conversation
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>
There was a problem hiding this comment.
💡 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".
| const elapsed = calendar.cells.filter((cell: UsageCalendarCell) => !cell.isFuture); | ||
| const inRange = timeline | ||
| ? elapsed.filter( | ||
| (cell) => cell.dayStartMs >= timeline.startMs && cell.dayStartMs <= timeline.endMs | ||
| ) |
There was a problem hiding this comment.
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 ? ( |
There was a problem hiding this comment.
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 👍 / 👎.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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>
There was a problem hiding this comment.
💡 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".
| const periodDates = `${dayFormat.format(new Date(stats.periodMs.fromMs))} – ${dayFormat.format( | ||
| new Date(stats.periodMs.toMs) | ||
| )}`; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| const hero = wide ? ( | ||
| <div className="flex min-w-0 items-baseline gap-2"> | ||
| {heroValue} | ||
| {heroUnits} | ||
| </div> |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 : ( |
There was a problem hiding this comment.
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 👍 / 👎.
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>
There was a problem hiding this comment.
💡 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 |
There was a problem hiding this comment.
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) => ( |
There was a problem hiding this comment.
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 👍 / 👎.
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>
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
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>
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 productone, 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.
that range's own timeline total, so the page and the image cannot disagree.
Every number is derived once in
usage-share-stats.ts.window is lit inside a dimmed year. All-time lights everything.
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 —
computeUsageShareMemberSlicesnever reads the emails thetimeline also holds, and a test asserts it.
chat-share-image-export.tsbecameshare-image-export.ts;chat-share-theme-scope.tsbecameshare-theme-scope.ts. Duplicating ~100 lines of snapdom, font readiness andElectron bridge handling into a second module was the alternative, and was
rejected.
shareCardprop with alazy dialog, because the public landing reuses
StatsSettingsViewand mustneither 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:quickpasses: oxlint (0 errors), i18n key parity, Code Collabimport guard, platform boundary guard, public repository boundary.
pnpm --filter @lody/components exec tsgo --noEmitpasses.pnpm run docs checkreports no errors;pnpm formatclean.tests/usage-share-stats.test.ts(7 cases) covers window-scoped streaks andaverages, 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) stillcovers browser download cleanup, native save cancellation/failure and invalid
captures.
measured to have zero content overflow, and PNG export was exercised end to end.
The full
@lody/componentssuite was run: 3312 of 3313 tests pass underNODE_ENV=development pnpm --filter @lody/components test. React 19 exportsactonly 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; thefile 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:page's range. Check
computeUsageShareStatsfor the day/interval split and thetotallighting rule, and that nothing recomputes a total locally.without the explicit toggle, and that the member mode really is gated on more
than one contributor.
ASPECT_SIZEis the whole exported imageincluding 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-0so a card thatdoes 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/sharerather than a dialog, which is a different interaction.