name: add-report
description: >
Guide for adding new report cards to ClaudeBar that analyze local data sources and display
metrics with comparison deltas. Use this skill when:
(1) Adding a new report/analytics card (e.g., weekly summary, model breakdown, session stats)
(2) Showing usage history in a new way, or reading another tool's usage logs
(3) Adding comparison cards that show "today vs previous" style deltas
(4) Building any feature that follows the DailyUsage pattern (days → report value → card)
Add Report Card to ClaudeBar
Add report cards that turn a login's usage history into metrics with comparison
deltas, shown in the existing card style, test first.
When to Use
This skill covers report-style features — cards that:
- Aggregate a login's days of usage (cost, tokens, time, sessions)
- Compare periods (today vs yesterday, this week vs last week)
- Read another tool's usage logs (as data in its definition)
- Display results in cards matching the existing UI
First, which kind of report is it?
Usage history is data now (daily-usage design).
A report is almost always a new view over days the login already reads, not new
reading code. Place it before anything else:
| The person wants… | It is | You write |
|---|
| a new look at usage this app already reads — this week vs last, cost per model, a 30-day chart | a report over days: the page asks account.usageHistory?.days(in: range) and a value in Quotas turns the days into the report | a Quotas value + its tests, a card. No reading code |
| the same history for another tool (its own log files) | a usageHistory block in that provider's definition | JSON (paths, where, fields, prices), its golden tests — no Swift |
| history from a log format no reader understands yet | a new reader, named for the format, in Modules/DataSources/Sources/Internal/Logs/ | the reader, test-first in DataSourcesTests, then the JSON; a row in ENGINE_DESIGN §1 |
| an answer to a different question than "how much did I use?" | a capability (CANONICAL §2.1): declared in the definition, a handle on Account that is nil when not declared | design first; a runner outside the modules is supplied by the Engine (TARGET_ARCHITECTURE §10) |
Never: a field on UsageSnapshot (the kernel is shrinking toward Usage,
CANONICAL_MODEL §6),
an XxxAnalyzer / XxxAnalyzing protocol, a parser in Sources/Infrastructure, or a
vendor-named type in a module.
Reference implementation: references/daily-usage-pattern.md — TODAY'S USAGE:
Claude's logs and prices as data, UsageLog in DataSources, account.usageHistory,
DailyUsageReport in Quotas, DailyUsageCardView. The Leaderboard reads 30 days the
same way (login.history.days(in:) → DailyTokens.summed).
Check the design first — the docs are the source of truth (AGENTS.md):
USER_JOURNEYS, CANONICAL_MODEL, TARGET_ARCHITECTURE, then the feature's design.md.
How a report over days flows
<id>.json "usageHistory" ──► UsageLog (DataSources) one per login, built by
reader · PriceList · ProviderFactory from the definition
DayAggregator
│
popover opens (never in the background, #204)
│ ▼
account.usageHistory?.days(in: .last(14)) ──► [DailyUsageStat] (closed days from the ledger)
│
▼
WeeklyReport(days:) a rich value in Modules/Quotas: deltas, percentages, formatting
│
▼
WeeklyCardView renders what the report says — never compares or counts
Workflow
Phase 0: Place it and design it (get user approval)
↓
Phase 1: The report value + tests (TDD Red→Green)
↓
Phase 2: Only if needed — the definition's usageHistory, a new reader, or a capability
↓
Phase 3: Mockup, card view, reading it on popover open
↓
Phase 4: Verify, screenshots on mock data, docs
Phase 0: Place it and design it (MANDATORY)
Step 1: Define the report
- What does the person want to know? In their words (USER_JOURNEYS).
- Which kind is it? The table above.
- Which days?
DateRange (.last(n), a week, today vs yesterday).
- Which logins? A report is per login — two logins are two reports, never summed.
- How many cards? One per metric, or one combined card.
Step 2: Diagram it
Example: this week vs last week, per login
┌────────────────────────────────────────────────────────────────────┐
│ Providers (module) Quotas (module) App │
│ │
│ account.usageHistory ─► WeeklyReport(days:) ─► WeeklyCardView │
│ .days(in: .last(14)) thisWeek / lastWeek × 3 metrics │
│ → [DailyUsageStat] deltas, %, formatted │
│ │
│ No reading code: claude.json's usageHistory already reads the logs│
└────────────────────────────────────────────────────────────────────┘
Step 3: The pieces
| Piece | Module | One job | Test |
|---|
WeeklyReport | Quotas | turns days into the comparison the card shows | Tests/DomainTests/<Feature>/ |
WeeklyCardView | App | renders it | AppTests / screenshots |
(only if needed) usageHistory block, a reader, a capability | definition / DataSources / Providers | — | golden tests / DataSourcesTests / ProvidersTests |
Step 4: Mockup and approval
A UI change gets a mockup in design-concept/<feature>/ on mock data first
(AGENTS.md). Present the doc change, the diagram, the pieces and the mockup;
use AskUserQuestion and wait for approval.
Phase 1: The report value (TDD)
Name each test should <outcome> [when <situation>], in the person's words, never a method, type or mechanism verb → Naming tests.
Location: Modules/Quotas/Sources/{Name}Report.swift (beside DailyUsageReport),
tests in Tests/DomainTests/{Feature}/{Name}ReportTests.swift (beside DailyUsageReportTests).
It is built from days — [DailyUsageStat] — so it never reads a file:
public struct {Name}Report: Sendable, Equatable {
public let current: [DailyUsageStat] // e.g. this week
public let previous: [DailyUsageStat] // e.g. last week
/// The days split at `boundary`, as the page asks for them.
public init(days: [DailyUsageStat], splitAt boundary: Date) { … }
public var cost: Decimal { current.reduce(0) { $0 + $1.totalCost } }
public var costChangePercent: Double? {
let before = previous.reduce(Decimal(0)) { $0 + $1.totalCost }
guard before > 0 else { return nil } // nil when there was nothing before
…
}
public var formattedCostDelta: String { … } // "+$5.00", "-$1.20": the sign always shown
public var costProgress: Double { … } // current / (current + previous), 0…1
}
Rules:
Decimal for money, TimeInterval for durations; Locale(identifier: "en_US") for currency.
- Every formatted string and every comparison lives in the value — the view only reads them.
- A change percent is
nil when the previous period is zero; deltas always carry a sign.
isEmpty means all zero (&&, not ||).
Write the tests first (state and return values, Chicago school), then the value.
Phase 2: Only if the report needs reading it doesn't have
2a. Another tool's history → its definition
Add a usageHistory block to <id>.json — files, format, where, fields,
prices, sessionGap — the shape is in the daily-usage design §2.
Golden tests in Modules/Providers/Tests/ over fixture logs (see ClaudeUsageHistoryTests).
No Swift.
2b. A log format no reader knows → one reader in DataSources
A reader named for the format (JSONLinesReader, never AcmeLogReader) in
Modules/DataSources/Sources/Internal/Logs/, test-first in
Modules/DataSources/Tests/Logs/ with neutral fixtures, yielding the one
LogRecord every reader yields. Then the JSON uses it. Add its row to
ENGINE_DESIGN §1. Only scan files changed within the range (LogFileFinder).
2c. A different question → a capability
Design it in the docs first (CANONICAL §2.1, TARGET_ARCHITECTURE): declared in
the definition, its own types in Modules/Providers, an @Observable handle on
Account that is nil when not declared — like usageHistory and
guestPasses. ProviderFactory.make builds it from the definition; a runner
that lives outside the modules (a CLI the App runs, the Keychain) is supplied by
the Engine the App builds once. The App never passes anything per provider.
Phase 3: The card, read on popover open
3a. The card view
Location: Sources/App/Views/{Name}CardView.swift. Match the existing
cards; DailyUsageCardView is the reference:
struct {Name}CardView: View {
let metric: {Name}Metric // one case per card
let report: {Name}Report
let delay: Double // cascading entrance
@Environment(\.appTheme) private var theme
var body: some View {
VStack(alignment: .leading, spacing: 6) {
// header: icon + LABEL · the large value · the progress bar · the delta line
}
.padding(12)
.background(/* theme.cardGradient fill + theme.glassBorder stroke */)
}
}
Card styling checklist: .padding(12) · theme.cardGradient + theme.glassBorder (1pt) ·
theme.cardCornerRadius · theme.fontDesign on text · theme.textPrimary/Secondary/Tertiary ·
theme.progressTrack · hover scale 1.015 · progress bar animated with delay.
3b. Read the days when the popover opens
Usage history is read when the popover opens, never in the background (#204),
and per login — the page asks the selected login, as MenuContentView does:
.task(id: login.id) {
guard let history = login.usageHistory else { return } // nil: this login offers none
let days = await history.days(in: .last(14))
report = {Name}Report(days: days, splitAt: weekStart)
}
The view renders report; it never compares, counts or decides from the days itself.
3c. Nothing to register
ProviderFactory.make builds usageHistory from the definition, and every
provider is found by ProviderCatalog.detect() (TARGET_ARCHITECTURE §10). The
App passes nothing per provider.
Phase 4: Verify
tuist generate
xcodebuild test -workspace ClaudeBar.xcworkspace -scheme ClaudeBar-Workspace -destination 'platform=macOS,arch=arm64' — the full run; tuist test skips cached targets
- Run the real popover on mock data (
scripts/demo-screenshots.sh) — screenshots, never real names, emails or usage
- Docs in the same change: the feature's
README.md / design.md, one CHANGELOG line (≤300 chars), python3 scripts/gen-docs.py && python3 scripts/check-docs.py --strict
Checklist
Phase 0
Phase 1
Phase 2 (only if needed)
Phase 3
Phase 4