feat(multi-account): track multiple Claude.ai accounts in parallel#17
Open
jeanfw wants to merge 3 commits into
Open
feat(multi-account): track multiple Claude.ai accounts in parallel#17jeanfw wants to merge 3 commits into
jeanfw wants to merge 3 commits into
Conversation
Replaces the single-account model with a list of ClaudeAccount entries keyed in settings, the keychain, and the disk cache by per-account UUID. Each configured account gets its own NSStatusItem with a dedicated popover so the menu bar shows freelance and client usage side by side. Setup wizard now collects a label, and Settings exposes add/edit/rename/ remove for accounts. Existing installs migrate the legacy 'cached_organization_id' setting and 'default' keychain slot to the new schema on first bootstrap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-account users were getting "Usage Warning" / "Session Reset" notifications with no indication of which account fired them. Now the title is prefixed with the account label, e.g. "Client X — Session Reset". The body is left untouched. `accountLabel` is required on `evaluateThresholds` and optional on `sendThresholdNotification` / `sendResetNotification` (test notifications fired from Settings pass nil to keep the bare title). Known limitation (not addressed here): `NotificationState`'s warning / critical / reset trackers remain account-global, so account A hitting a threshold can briefly suppress account B's notification until A drops below. That's a separate refactor and the existing behaviour is unchanged vs. the rest of this PR.
163e08f to
419d08b
Compare
`NotificationState` kept `hasWarningBeenNotified`, `hasCriticalBeenNotified`
and `lastPercentage` as account-global flags. In multi-account mode every
refresh would overwrite the global with whichever account was checked
last — so when account A sat at 50% and account B at 0%, B repeatedly
satisfied the "reset detected" check (`lastPercentage > 0 && current == 0`)
because A had just bumped lastPercentage back to 50. Result: a "Session
Reset" notification fired on every refresh interval for any account
sitting at 0% while another account had non-zero usage.
Fix: store all three trackers per-account UUID:
- `hasWarningBeenNotified: [UUID: Bool]`
- `hasCriticalBeenNotified: [UUID: Bool]`
- `lastSessionPercentageByAccount: [UUID: Double]`
`evaluateThresholds` now takes an `accountId` parameter and indexes into
the dictionaries. Re-arming the warning/critical "already notified" flags
when utilization drops below the threshold is also scoped per-account.
Legacy single-account keys (`warning_notified`, `critical_notified`,
`last_percentage`) cannot be safely migrated — they have no account
attribution — so the per-account dicts start empty on upgrade. Worst
case: one extra reset notification after the upgrade if a user was sitting
at non-zero utilization at the moment of upgrade (the very first refresh
will write the percentage; the second refresh will see the value drop or
hold). Worth the cleanup.
Also clarifies the reset notification's title and body to specify the
5-hour session window (vs the weekly window, which doesn't fire reset
notifications today). New test cases cover:
- reset fires exactly once per real reset (subsequent refreshes at 0%
don't re-fire)
- a second account sitting at 0% never receives a spurious reset notif
even while another account is bouncing around non-zero values
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the single-account model with a list of
ClaudeAccountentries, each with its own keychain slot, disk cache file, andNSStatusItem. Useful when you have more than one Claude plan to keep an eye on — for example a personal account and a separate client account.What changes
ClaudeAccountmodel (id: UUID,label,organizationId). The label is user-facing; the id is the stable key for keychain and cache.AppSettingsdropscachedOrganizationIdin favour ofaccounts: [ClaudeAccount]. A customCodableinitialiser performs the migration: a saved blob withcached_organization_idand noaccountsis decoded into a single account labelledDefault.AppModelstores per-account fetch state inaccountStates: [UUID: AccountUsageState]and exposesrefreshUsage(accountId:),refreshAllUsage(),addAccount(label:sessionKey:),updateSessionKey(accountId:_:),renameAccount(_:label:),removeAccount(_:).CacheRepositoryandUsageServiceare now keyed by account UUID. The public~/.claudemeter/usage.jsonexport is written for the primary (first) account only, preserving compatibility with existing statusline scripts.MenuBarManagerreconciles oneNSStatusItemper account (plus a setup item when no account is configured). Each status item gets its own popover scoped to that account; when more than one account exists each icon is prefixed by the account's first letter so they're distinguishable at a glance.SetupWizardViewnow collects a label in addition to the session key.SettingsViewgains an Accounts section with add / rename / replace-session-key / remove actions.defaultkeychain slot exists and the first account's UUID slot is empty, the key is copied across and the legacy slot is deleted. Idempotent on subsequent launches.AppModelTests,UsageServiceTests,SettingsRepositoryTestsand theTestDoublesare updated to match the new API; a new test exercises the legacy-cached_organization_idmigration path.What stays the same
~/.claudemeter/usage.jsonschema (only the primary account is written).Test plan
cached_organization_idset in UserDefaults anddefaultsession key in Keychain): one account namedDefaultappears, usage loads, keychaindefaultslot is gone, the new UUID slot is populated.~/Library/Application Support/com.claudemeter/usage_<uuid>.jsonis purged.🤖 Generated with Claude Code