Skip to content

feat(status-bar): add Time Trends & Day Trends charts to the Claude usage popover#8406

Open
jellychoco wants to merge 2 commits into
stablyai:mainfrom
jellychoco:feat/claude-usage-trends-popover-charts
Open

feat(status-bar): add Time Trends & Day Trends charts to the Claude usage popover#8406
jellychoco wants to merge 2 commits into
stablyai:mainfrom
jellychoco:feat/claude-usage-trends-popover-charts

Conversation

@jellychoco

Copy link
Copy Markdown

What

Adds a chart panel to the right side of the status-bar Claude usage popover. The popover previously showed only the account rate-limit bars (Session / Weekly / Fable); it now also visualizes locally scanned Claude Code token usage:

  • Time Trends — an hour-of-day overlay line chart (X = 00–23h, one line per day, today emphasized; historical lines fade by window so overlap density reads as "the hours you use most"). Ranges: 1D / 7D / 30D / 6M. Hovering an hour shows Today / Avg (active days) / Peak.
  • Day Trends — total-token bars per day (7D / 30D) or per month (6M), with a window total in the header and a hover tooltip breaking down Input / Output / Cache read / Cache write. A leading partial month is trimmed from the 6M view so month totals compare honestly.
  • Mode and range persist across sessions (localStorage). Charts fill the panel via ResizeObserver-driven SVG sizing and densify gridlines on tall plots.
  • Usage tracking remains opt-in (same consent flag as Settings → Stats & Usage). Until enabled, the panel shows an enable CTA; all data stays on-device.

The charts are a port of the Time Trends / daily trend visualizations from my UsageScope app, adapted to Orca's per-provider popover (Claude-only series, --chart-3 token).

Screenshots

Before After (enable CTA)
before cta
After — Time Trends (30D) After — hour tooltip
time hover
After — Day Trends (30D) After — Day Trends (6M, monthly buckets)
day30 day6m

How it works

  • The existing src/main/claude-usage scanner now also aggregates hour-of-day buckets (day × hour, project/model-free so the persisted cache stays ≤ 24 rows per active day per file), with the same dedupe/fork-ownership semantics as daily aggregates ([Bug]: The stats and Usage #8006 unaffected).
  • SCHEMA_VERSION 5 → 6: old caches are dropped and reparsed on next refresh; scanState.enabled is preserved (existing bump behavior).
  • New claudeUsage:getHourly IPC serves hourly points clamped to a 185-day ceiling (6M window + slack), following the store's existing query-time-filtering convention.
  • ProviderDetailsMenu gains an opt-in sideContent prop (popover widens 260px → 588px into a two-column layout). Only the Claude menu uses it; Codex/Gemini/other provider popovers are untouched.

Tests

13 new tests: hourly aggregation across projects/hours (scanner), window filtering + 185-day clamp (store), day/month bucketing, partial-month trim, zero-fill, monotone-cubic path edge cases, and tooltip stats (renderer model). All model logic lives in a pure .ts module so the tests exercise the real chart math.

AI code review summary (Claude)

An adversarial multi-agent review (3 reviewer dimensions, each finding independently re-verified against the code) ran on this diff:

  • Cross-platform: no platform APIs added; date bucketing uses local-calendar Date math consistent with the existing scanner; no keyboard shortcuts, no path handling beyond the existing scanner roots. Tests avoid timezone-dependent assertions.
  • SSH / remote / local: the scanner is local-only by design, so the panel is hidden when a remote runtime environment is active — the popover then shows the remote server's account (Remote provider account UI silently reads and mutates local accounts #7973) and pairing it with local-disk charts would misattribute data. Empty/CTA copy explicitly says "local … transcripts". Flagged during review and fixed in this PR.
  • Agents / integrations / providers: feature is scoped to the Claude popover behind an opt-in prop; no shared provider code paths change behavior for other providers or git providers.
  • Performance: one IPC fetch per popover open (backed by the store's existing 5-minute scan staleness gate); hourly payload is bounded (≤ 185 × 24 points); charts memoize models and render plain SVG; per-file cache reuse is preserved so unchanged transcripts still pay only stat() on rescans. The schema bump costs one full reparse on first refresh (same as previous bumps).
  • UI quality: STYLEGUIDE tokens only (--chart-3, border, muted, popover, text tokens); works in light/dark; hover tooltips on both charts; loading/error/empty/consent states all render distinctly.
  • Security / privacy: no new network calls; transcript data never leaves the device; tracking stays behind the existing consent flag; a rejected IPC can no longer masquerade as the "tracking disabled" consent state (reviewed and fixed).

Known limitation (pre-existing pattern): controls inside the Radix DropdownMenu popover are not keyboard-reachable (Radix menus block Tab and rove only across menu items) — same as the existing AccountRuntimeToggle in this popover. Usage tracking can also be enabled from Settings → Stats & Usage, which is keyboard-accessible.

Notes

  • New i18n keys were added via pnpm run sync:localization-catalog; es/ja/ko/zh currently carry the English source values per the catalog bootstrap flow.
  • Checks run locally: pnpm lint, pnpm typecheck, pnpm test, and pnpm build:desktop (native build unchanged by this PR). The two full-suite test failures on my machine are pre-existing environment issues (missing node-pty spawn-helper binary; a locale-dependent assertion in automation-schedules.test.ts on a Korean-locale system) and reproduce on a clean checkout.
  • Verified by driving the built app with Playwright: enable flow, both charts, all ranges, hover tooltips, and preference persistence across relaunch (screenshots above are from those runs).

X: @Command_CV

🤖 Generated with Claude Code

… popover

Add a chart panel to the right side of the status-bar Claude usage
popover, porting the "Time Trends" hour-of-day overlay and daily trend
charts from UsageScope. The popover previously showed only rate-limit
bars (Session/Weekly/Fable); it now also charts locally scanned token
usage history so users can see when and how much they use Claude.

User-visible behavior:
- Time Trends: hour-of-day overlay line chart (X = 00-23h). One line
  per day, today emphasized at 2px full opacity, historical days at
  1px with per-window opacity (1D 0.9 / 7D 0.38 / 30D 0.2 / 6M 0.08)
  so overlap density reads as "hours I use most". Hover shows a
  per-hour tooltip: Today / Avg (active days) / Peak.
- Day Trends: total-token bars per day (7D/30D) or per month (6M),
  hover tooltip with Total/Input/Output/Cache read/Cache write.
  A leading partial month is trimmed from the 6M view so month totals
  compare honestly.
- Mode (Time/Day) and range (1D/7D/30D/6M) persist via localStorage.
- Charts fill the panel height via ResizeObserver-driven SVG sizing;
  gridline density increases (2 -> 4 lines) on plots >= 200px tall.
- Usage tracking stays opt-in: the panel shows an enable CTA until
  claude-usage scanning is turned on; data never leaves the device.
- The panel is hidden while a remote runtime environment is active:
  the popover then shows the remote account (stablyai#7973) and pairing it
  with local-disk charts would misattribute the data.

Data pipeline (main process):
- claude-usage scanner now aggregates hour-of-day buckets (day x hour,
  project/model-free key to keep the cache small) alongside daily
  aggregates, merged across files with the same dedupe/ownership
  semantics (stablyai#8006 fork reclaim unaffected).
- SCHEMA_VERSION 5 -> 6: old caches lack per-file hourly projections,
  so they are dropped and fully reparsed on next refresh;
  scanState.enabled is preserved across the bump (existing behavior).
- New query getHourly(days): serves hourly points clamped to
  HOURLY_RETENTION_DAYS = 185 (6M window + slack) via new IPC channel
  claudeUsage:getHourly, exposed on window.api.claudeUsage.

Per-file changes:
| File | Change |
| --- | --- |
| src/main/claude-usage/types.ts | ClaudeUsageHourlyAggregate; hourlyAggregates on persisted state/file; hour on attributed turn |
| src/main/claude-usage/scanner.ts | local day+hour bucketing, hourly aggregation/merge/sort, hourly in per-file reuse check and scan result |
| src/main/claude-usage/store.ts | SCHEMA_VERSION 5->6, HOURLY_RETENTION_DAYS=185, getHourly/buildHourly, getDayCutoff extracted from getRangeCutoff |
| src/main/ipc/claude-usage.ts | claudeUsage:getHourly handler |
| src/preload/index.ts, api-types.ts | claudeUsage.getHourly bridge + ClaudeUsageHourlyResult typing |
| src/shared/claude-usage-types.ts | ClaudeUsageHourlyPoint, ClaudeUsageHourlyResult DTOs |
| src/renderer/.../ClaudeUsageTrendsPanel.tsx | new: panel shell, mode/range segmented controls, fetch + enable/error/empty/loading states |
| src/renderer/.../claude-usage-trends-model.ts | new: pure model — hour-of-day matrix, day/month buckets, monotone-cubic path builder, tooltip stats |
| src/renderer/.../claude-usage-trend-charts.tsx | new: TimeTrendsChart + DayTrendsChart SVG rendering and hover hit-testing |
| src/renderer/.../claude-usage-trend-chart-frame.tsx | new: shared chart scaffolding — dims hook (ResizeObserver), Y axis, tooltip surface |
| src/renderer/.../StatusBar.tsx | ProviderDetailsMenu sideContent prop (260px -> 588px two-column layout when present); Claude menu passes the panel, gated off remote runtimes |
| src/main/claude-usage/*.test.ts, .../claude-usage-trends-model.test.ts | 13 new tests: hourly aggregation, window/185d clamp, day/month bucketing, partial-month trim, monotone path, tooltip stats |
| src/renderer/src/i18n/locales/*.json | 28 new auto.* keys via pnpm run sync:localization-catalog |

Design decisions:
- Hourly aggregates are account-wide (no scope/model/project dimension)
  because the popover complements account-level rate limits; persisted
  size stays <= 24 rows per active day per file.
- Retention is enforced at query time (matching the store's existing
  "no pruning, filter at query" convention); 185 days is the ceiling
  the API will serve regardless of requested window.
- Charts are hand-rolled SVG with --chart-3 tokens (no chart library,
  matching the existing stats-pane convention of hand-rolled charts).

Known limitation: controls inside the Radix DropdownMenu popover are
not keyboard-reachable (Radix menus block Tab and rove only across
menu items). This matches the pre-existing AccountRuntimeToggle in the
same popover; usage tracking can also be enabled from Settings > Stats
& Usage, which remains keyboard-accessible.

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

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a494cd53-c6f0-4836-b0ff-f51f12fadf74

📥 Commits

Reviewing files that changed from the base of the PR and between c48d8eb and bd711c6.

📒 Files selected for processing (7)
  • src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx
  • src/renderer/src/components/status-bar/claude-usage-trend-charts.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/components/status-bar/claude-usage-trend-charts.tsx
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx

📝 Walkthrough

Walkthrough

Adds hourly Claude usage attribution, aggregation, persistence, retention-limited retrieval, and IPC/preload APIs. Introduces renderer models and SVG charts for hourly, daily, and monthly trends, with local preferences, loading and tracking states, tooltips, and localized labels. Integrates the trends panel into the Claude status-bar menu when no remote runtime is active.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding Claude usage trend charts in the status-bar popover.
Description check ✅ Passed The description covers the main sections and details, though it lacks an explicit Security Audit section and exact template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

❤️ Share

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx (2)

89-115: 🩺 Stability & Availability | 🔵 Trivial

No timeout on the getHourly IPC round trip.

If the main-process scan (or the underlying refresh() it triggers) stalls, the "Scanning local transcripts…" state can persist indefinitely with no fallback for the user. Consider wrapping this call with a bounded timeout so a stuck scan surfaces the loadFailed state instead of hanging silently.


41-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the custom segmented control with the existing ToggleGroup primitive. src/renderer/src/components/ui/toggle-group.tsx already exists, so reusing it keeps keyboard/focus behavior and styling aligned with the rest of the UI.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0f4944b0-ddc4-4109-b39b-1e7010bea54b

📥 Commits

Reviewing files that changed from the base of the PR and between 26934b1 and c48d8eb.

📒 Files selected for processing (20)
  • src/main/claude-usage/scanner.test.ts
  • src/main/claude-usage/scanner.ts
  • src/main/claude-usage/store.test.ts
  • src/main/claude-usage/store.ts
  • src/main/claude-usage/types.ts
  • src/main/ipc/claude-usage.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx
  • src/renderer/src/components/status-bar/StatusBar.tsx
  • src/renderer/src/components/status-bar/claude-usage-trend-chart-frame.tsx
  • src/renderer/src/components/status-bar/claude-usage-trend-charts.tsx
  • src/renderer/src/components/status-bar/claude-usage-trends-model.test.ts
  • src/renderer/src/components/status-bar/claude-usage-trends-model.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/shared/claude-usage-types.ts

Comment thread src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx
Comment thread src/renderer/src/components/status-bar/ClaudeUsageTrendsPanel.tsx
Review: stablyai#8406 (CodeRabbit, 3 actionable + 2 nitpicks)

Per-file changes:
| File | Change |
| --- | --- |
| claude-usage-trend-charts.tsx | date axis/tooltip labels now use the active UI locale (i18n.language) instead of the OS locale (toLocaleDateString undefined -> i18n.language) |
| ClaudeUsageTrendsPanel.tsx | SegmentedControl reimplemented on the ui/toggle-group shadcn primitive (guarding Radix's empty-value emit on re-click); Enable button disabled while loading to prevent overlapping setEnabled/getHourly calls; mode toggle gets its own aria-label 'Chart mode' (new key 53ce9bcf71) instead of duplicating the region label |
| i18n/locales/*.json | 'Chart mode' key synced across 5 catalogs |

Declined (with rationale in PR thread): client-side timeout on the
getHourly IPC — first scans of large transcript histories legitimately
run long and the IPC resolves when the scan completes; a fixed timeout
would misreport in-progress scans as failures, and scan errors already
surface via scanState.lastScanError.

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

Copy link
Copy Markdown
Author

Addressed the CodeRabbit review in bd711c6:

  • Locale-aware date labels — chart axis/tooltip dates now format with the active UI locale (i18n.language) instead of the OS locale.
  • Enable button double-click guarddisabled={isLoading} prevents overlapping setEnabled/getHourly calls.
  • Distinct aria-labels — the Time/Day mode toggle now announces "Chart mode" instead of duplicating the region label.
  • ToggleGroup primitive (nitpick) — the custom segmented control is now built on ui/toggle-group (Radix), with a guard against the empty value Radix emits when the active item is re-clicked. Visuals verified unchanged in the running app.

Declining one nitpick with rationale — client-side timeout on the getHourly IPC: the first scan of a large transcript history legitimately runs long, and the IPC resolves when the scan completes, so "Scanning local transcripts…" accurately reflects in-progress work. A fixed timeout would misreport those scans as failures; genuine scan errors already surface through scanState.lastScanError (rendered by the panel's empty/error state), and a rejected IPC lands in the loadFailed state.

Re-ran pnpm lint, pnpm typecheck, and the claude-usage + status-bar test suites (207 passing) after the changes.

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.

1 participant