feat: multi-profile usage monitoring with real-time rate limit bars#84
feat: multi-profile usage monitoring with real-time rate limit bars#84kvnloo wants to merge 2 commits into
Conversation
Add comprehensive usage monitoring for users with multiple Claude Code profiles (e.g., separate home/work accounts). Sessions are grouped by profile in the instances list, with per-profile usage tracking. Features: - Profile-aware session grouping via --profile flag in hook script - Real-time usage limit bars (session 5h + weekly 7d) sourced from Anthropic's rate_limits API via Claude Code's status line system - Cost estimation from JSONL conversation logs with per-model pricing and message deduplication - Burn rate display (tokens/min) - Lifetime savings calculation (API cost vs subscription fee) - Daily/monthly cost history in the settings menu - Auto-detection of subscription plan from macOS Keychain - Configurable display: toggles for cost, burn rate, savings, and individual limit bars, plus per-profile plan override - Scrollable settings menu to prevent overflow Architecture: - UsageModels.swift: data types for plans, pricing, limits, costs - UsageTracker.swift: actor for JSONL parsing and rate limit state - UsageSettings.swift: @mainactor ObservableObject for preferences - PlanDetector.swift: keychain + .claude.json plan auto-detection - StatusLine integration: shell script writes rate_limits to ~/.claude-island/{profile}.json for the app to read Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 008c04f66b
ℹ️ 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".
| if let profile = event.profile { | ||
| session.profile = profile | ||
| Task { await UsageTracker.shared.registerProfile(profile) } | ||
| } |
There was a problem hiding this comment.
Register a default profile when profile is missing
Usage tracking is gated behind if let profile = event.profile, so sessions only get registered with UsageTracker when hooks explicitly send a profile. In the current flow the installed hook command still runs without --profile, and the Python hook omits profile in that case, which means default-profile users never populate profileUsage and won’t see the new usage bars/history unless they manually patch their hook config.
Useful? React with 👍 / 👎.
| // Calculate lifetime savings for any profiles we haven't computed yet | ||
| let missingProfiles = profileUsage.keys.filter { profileLifetimeSavings[$0] == nil } | ||
| if !missingProfiles.isEmpty { | ||
| var lt = profileLifetimeSavings |
There was a problem hiding this comment.
Recompute lifetime savings after plan changes
This block only computes lifetime savings for profiles that are not already cached, so once a profile is initialized, changing its plan override later does not refresh totalSubscriptionCost. The UI can then continue showing stale lifetime savings based on the previous monthly fee, which is incorrect for the settings-driven plan override workflow introduced in this change.
Useful? React with 👍 / 👎.
| let formatter = ISO8601DateFormatter() | ||
| formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] | ||
| return formatter.date(from: dateStr) |
There was a problem hiding this comment.
Parse subscription dates without requiring fractional seconds
The ISO-8601 parser is configured with .withFractionalSeconds, which fails on valid timestamps like 2026-04-01T12:00:00Z that omit fractional seconds. When parsing fails, downstream code falls back to months = 1, materially skewing lifetime subscription-cost and savings calculations for older accounts.
Useful? React with 👍 / 👎.
- Register "default" profile when --profile flag is not provided, so users without custom hook config still get usage tracking - Invalidate cached lifetime savings when plan is changed via settings, so the subscription cost recalculates with the new monthly fee - Parse ISO 8601 subscription dates with and without fractional seconds, preventing fallback to months=1 for timestamps like "2026-04-01T12:00:00Z" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…farouqaldori#84) * fix: eliminate persistent Keychain access prompts for CLI OAuth token PR farouqaldori#75 added a caching layer but prompts persisted because: 1. Expired cached tokens were deleted, forcing a CLI Keychain re-read 2. The rate-limit timestamp was in-memory and reset on every app launch Changes: - Return cached OAuth tokens regardless of local expiry, deferring validation to the API (401/403 triggers cache invalidation) - Persist rate-limit timestamp to UserDefaults so cooldown survives app restarts - Increase cooldown from 5 minutes to 1 hour - Disable SwiftFormat's redundantSendable rule to prevent incorrect stripping of Sendable conformances under Swift 6.2 * fix: reset keychain cooldown on auth failure to allow recovery
Summary
Adds comprehensive usage monitoring for users with multiple Claude Code profiles (e.g., separate home/work accounts with different subscription plans).
--profileflag in the hook scriptrate_limitsAPI, sourced via Claude Code's status line system. No guessing at limits — uses the actual percentages from Anthropic's backendClaude Code-credentials-*entries). Two-pass keychain query: finds matching services first (attributes only), then fetches credential data only for Claude Code entriesArchitecture
Models/UsageModels.swiftServices/State/UsageTracker.swiftServices/Hooks/PlanDetector.swift.claude.jsonplan auto-detectionCore/UsageSettings.swift@MainActor ObservableObjectfor display preferences and plan configUI/Components/UsageSettingsRow.swiftUI/Components/UsageHistorySection.swiftThe rate limit bars require a status line script that writes
rate_limitsJSON from Claude Code to~/.claude-island/{profile}.json. This is the only way to get accurate utilization percentages — the JSONL files don't contain rate limit data, and Anthropic doesn't expose a public API for it.Setup
Requires configuration in each profile's
settings.json:{ "statusLine": { "type": "command", "command": "CLAUDE_ISLAND_PROFILE=home /path/to/claude-island-statusline.sh" } }The hook script also needs
--profile <name>added to the command.Test plan
xcodebuild -scheme ClaudeIsland -configuration Debug build🤖 Generated with Claude Code