Skip to content

feat: multi-profile usage monitoring with real-time rate limit bars#84

Open
kvnloo wants to merge 2 commits into
farouqaldori:mainfrom
kvnloo:feat/usage-monitoring
Open

feat: multi-profile usage monitoring with real-time rate limit bars#84
kvnloo wants to merge 2 commits into
farouqaldori:mainfrom
kvnloo:feat/usage-monitoring

Conversation

@kvnloo

@kvnloo kvnloo commented Apr 11, 2026

Copy link
Copy Markdown

Summary

Adds comprehensive usage monitoring for users with multiple Claude Code profiles (e.g., separate home/work accounts with different subscription plans).

  • Profile-aware session grouping — sessions are split by profile in the instances list via a --profile flag in the hook script
  • Real-time usage limit bars — session (5h) and weekly (7d) utilization from Anthropic's rate_limits API, sourced via Claude Code's status line system. No guessing at limits — uses the actual percentages from Anthropic's backend
  • Cost estimation — per-session API cost calculated from JSONL conversation logs with per-model pricing and message deduplication
  • Burn rate — tokens/min display
  • Lifetime savings — total API cost vs subscription fees since account creation
  • Daily/monthly cost history — expandable section in the settings menu
  • Plan auto-detection — reads subscription type and rate limit tier from macOS Keychain (Claude Code-credentials-* entries). Two-pass keychain query: finds matching services first (attributes only), then fetches credential data only for Claude Code entries
  • Configurable display — toggles for cost, burn rate, savings, lifetime savings, and individual limit bars. Per-profile plan override in settings
  • Scrollable settings menu — prevents overflow when multiple sections are expanded

Architecture

File Purpose
Models/UsageModels.swift Data types: plans, pricing, limits, costs, display config
Services/State/UsageTracker.swift Actor for JSONL parsing, rate limit state reading, cost calculation
Services/Hooks/PlanDetector.swift Keychain + .claude.json plan auto-detection
Core/UsageSettings.swift @MainActor ObservableObject for display preferences and plan config
UI/Components/UsageSettingsRow.swift Settings toggles + per-profile plan picker
UI/Components/UsageHistorySection.swift Daily/monthly cost history + lifetime savings

The rate limit bars require a status line script that writes rate_limits JSON 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

  • Build with xcodebuild -scheme ClaudeIsland -configuration Debug build
  • Launch app with multiple Claude Code profiles active
  • Verify sessions are grouped by profile
  • Verify usage bars match claude.ai/settings/usage percentages
  • Open settings, toggle display options, verify UI updates
  • Expand Usage History section, verify daily/monthly data
  • Verify settings persist across app restarts
  • Test with single profile (no grouping should appear)
  • Test without status line configured (bars should not appear)

🤖 Generated with Claude Code

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +129 to +132
if let profile = event.profile {
session.profile = profile
Task { await UsageTracker.shared.registerProfile(profile) }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +196 to +199
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +218 to +220
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.date(from: dateStr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
rosuH pushed a commit to rosuH/claude-island that referenced this pull request Apr 14, 2026
…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
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