From 54bdf4a1e6330e46a09e516cc0d858fa3b92a49d Mon Sep 17 00:00:00 2001 From: JW-Albert Date: Sun, 21 Jun 2026 00:24:55 +0800 Subject: [PATCH] Stats: hide Claude/Codex breakdown when a service filter is active The breakdown duplicated Total cost once filtered to a single service. Also gitignore .claude/ and add CLAUDE.md project notes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + CLAUDE.md | 57 ++++++++++++++++++++ Sources/AgentMeter/Stats/StatsRootView.swift | 5 +- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 175d7a1..b3fdc70 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist/ AgentMeter.app/ # Original drop-in icon artwork; the committed source lives in Resources/AppIcon.png AgentMeter_icon*.jpg + +.claude/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d77afb0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +AgentMeter — a macOS 14+ menu-bar app (Swift / AppKit + SwiftUI) that reads +local Claude Code, Claude (status), and Codex usage logs and shows a live +usage/cost gauge. + +## Build & test + +```bash +swift build # build all targets +swift test # run AgentMeterCoreTests +./Scripts/bundle.sh # produce the .app bundle +./Scripts/release.sh # build + bundle a release +``` + +Two CI workflows: `gitleaks` (secret scan) and `offline-check` +(`Scripts/check-offline.sh` — the app must work with no network by default). + +## Layout + +`Sources/AgentMeterCore` — **core layer**, no AppKit/SwiftUI (high fan-in, +~1 outbound). Pure parsing/pricing/formatting; the only thing tested. +`Sources/AgentMeter` — the menu-bar app (UI, networking, settings). +`Tests/AgentMeterCoreTests` — covers Core only. +`Scripts/` — bundle/release/icns/statusline/offline-check shell + swift. + +## Core (`AgentMeterCore`) — the pieces that matter + +- `CodexReader`, `ClaudeCodeReader`, `ClaudeStatusReader` — parse each + agent's local logs. `CodexReader.read` is the hottest symbol (fan-in 18); + `read`/`history` are the reader entry points. +- `UsageReader` / `UsageModels` / `UsageHistory` — the usage data model. +- `Pricing.costEstimate` — token → cost (fan-in 8). `ModelIdentity`, + `ModelContextWindow` — model id → context window / identity. +- `StatusLevel.forUsed` — maps usage fraction to a band (normal/warning/low/ + empty); drives gauge color. +- `DateWindows` (`isToday`, `isInRollingWindow`) — rolling/daily windows. +- `CoreSupport.parseISOTimestamp`, `NumberFormatting`, `HexColor` — helpers. +- `Credentials` — parses `ClaudeCredentials` / `ServiceAccount` payloads. + +## App (`AgentMeter`) + +- `AgentMeterApp` — `applicationDidFinishLaunching`, menu-bar setup. +- `UsageStore.refreshNow` — refresh loop feeding the UI (fan-in 5). +- `MenuBarMetric` / `MenuBarIconPath` / `MeterBar` — menu-bar gauge drawing. +- `MenuContentView`, `SettingsView`, `Stats/*` — popover, settings, stats window. +- `Floating/*` — optional floating HUD panel. +- `Network/*` — `CodexQuotaClient`, `CodexTokenRefresher`, `NetworkOptIn`. + All network is **opt-in**; default is offline (enforced by CI). +- `Localization.LanguageStore.tr` — string lookup (fan-in 12); UI is localized. + +## Conventions + +- Keep `AgentMeterCore` UI-free — it's meant to lift into a Stats module + unchanged (see `Package.swift` comment). Don't import AppKit/SwiftUI there. +- New logic lands in Core with a test in `AgentMeterCoreTests`. +- Offline-by-default: anything touching the network goes through `NetworkOptIn`. \ No newline at end of file diff --git a/Sources/AgentMeter/Stats/StatsRootView.swift b/Sources/AgentMeter/Stats/StatsRootView.swift index 7b17282..0a9d1af 100644 --- a/Sources/AgentMeter/Stats/StatsRootView.swift +++ b/Sources/AgentMeter/Stats/StatsRootView.swift @@ -151,7 +151,10 @@ struct StatsRootView: View { HStack(spacing: 34) { kpi(lang.tr("Total tokens", "總 tokens"), Int(totalTokens).compactTokenString) kpi(lang.tr("Total cost", "總花費"), moneyString(totalCost)) - kpi("Claude / Codex", "\(moneyString(cost(.claudeCode))) · \(moneyString(cost(.codex)))", small: true) + // ponytail: breakdown only meaningful in .all; otherwise it duplicates Total cost + if service == .all { + kpi("Claude / Codex", "\(moneyString(cost(.claudeCode))) · \(moneyString(cost(.codex)))", small: true) + } } Rectangle().fill(AM.hairline).frame(height: 1) sectionHeader(lang.tr("Daily usage", "每日用量"))