From f9fe8b38774c0ae3e8eec0f9489ab7d350289636 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:51:33 -0400 Subject: [PATCH 1/4] Handle transient CloudKit sync failures --- .../Models/SyncedUsageData.swift | 30 +++++++++++++------ .../CodexBarMobileTests/SyncErrorTests.swift | 16 ++++++++++ Scripts/verify-cloudkit-schema.sh | 12 ++++++++ Shared/iCloud/CloudSyncManager.swift | 25 ++++++++++++++++ docs/cloudkit-deploy-audit.md | 8 +++++ 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift b/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift index b3a6344b..9ae8c064 100644 --- a/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift +++ b/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift @@ -356,11 +356,7 @@ final class SyncedUsageData { // unreachable. Surface the error in status but leave `snapshot` // pointing at whatever was hydrated / from last successful fetch. if perArg == nil && legacyArg == nil { - if let firstError { - self.syncStatus = .error(message: firstError.description) - } else { - self.syncStatus = .noData - } + self.applyRefreshFailureStatus(firstError) return } @@ -384,10 +380,10 @@ final class SyncedUsageData { self.republishFromCache() return } - if let firstError { - self.syncStatus = .error(message: firstError.description) - } else { - self.syncStatus = .noData + self.applyRefreshFailureStatus(firstError) + if firstError != nil, let snapshot { + WidgetSnapshotPublisher.publish(from: snapshot) + return } self.snapshot = nil WidgetSnapshotPublisher.clear() @@ -559,6 +555,22 @@ final class SyncedUsageData { } } + private func applyRefreshFailureStatus(_ error: CloudSyncError?) { + if let snapshot { + // Refresh failures are not data failures. Keep cached Mac data in + // the normal freshness model so a transient CloudKit/indexing blip + // does not make the app look broken while the user still has data. + self.syncStatus = .synced(lastConfirmedSync: snapshot.syncTimestamp) + return + } + + if let error { + self.syncStatus = .error(message: error.description) + } else { + self.syncStatus = .noData + } + } + // MARK: - Public API /// Force-refreshes data from CloudKit (full fetch). diff --git a/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift b/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift index c93a322e..a0c87f6e 100644 --- a/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift @@ -58,6 +58,22 @@ struct SyncErrorTests { } } + @Test("Queryable recordName error maps to Production index issue") + func queryableRecordNameError() { + let ckError = CKError( + .invalidArguments, + userInfo: [ + NSLocalizedDescriptionKey: "Field 'recordName' is not marked queryable", + ]) + let syncError = CloudSyncError(from: ckError) + + if case .productionSchemaMissingQueryableIndex(let fieldName) = syncError { + #expect(fieldName == "recordName") + } else { + Issue.record("Expected .productionSchemaMissingQueryableIndex, got \(syncError)") + } + } + // MARK: - SyncStatus properties @Test("SyncStatus.error isError returns true") diff --git a/Scripts/verify-cloudkit-schema.sh b/Scripts/verify-cloudkit-schema.sh index d7122827..effd3d52 100755 --- a/Scripts/verify-cloudkit-schema.sh +++ b/Scripts/verify-cloudkit-schema.sh @@ -119,12 +119,22 @@ require_queryable_field() { fi } +require_queryable_record_name() { + local record_type="$1" + if ! queryable_field_exists "$record_type" "recordName" \ + && ! queryable_field_exists "$record_type" "___recordID" + then + failures+=("missing queryable index: ${record_type}.recordName") + fi +} + require_record_type "DeviceSnapshot" require_field "DeviceSnapshot" "deviceName" require_field "DeviceSnapshot" "deviceID" require_field "DeviceSnapshot" "appVersion" require_field "DeviceSnapshot" "syncTimestamp" require_field "DeviceSnapshot" "payload" +require_queryable_record_name "DeviceSnapshot" require_record_type "DeviceProviderSnapshot" require_field "DeviceProviderSnapshot" "deviceID" @@ -136,6 +146,7 @@ require_field "DeviceProviderSnapshot" "lastUpdated" require_field "DeviceProviderSnapshot" "encodingVersion" require_field "DeviceProviderSnapshot" "payload" require_queryable_field "DeviceProviderSnapshot" "deviceID" +require_queryable_record_name "DeviceProviderSnapshot" require_record_type "ProviderAccountLinkage" require_field "ProviderAccountLinkage" "providerID" @@ -143,6 +154,7 @@ require_field "ProviderAccountLinkage" "linkedIdentifiers" require_field "ProviderAccountLinkage" "confirmedAt" require_field "ProviderAccountLinkage" "confirmedFromDeviceID" require_field "ProviderAccountLinkage" "unmerge" +require_queryable_record_name "ProviderAccountLinkage" require_record_type "QuotaTransition" require_field "QuotaTransition" "providerName" diff --git a/Shared/iCloud/CloudSyncManager.swift b/Shared/iCloud/CloudSyncManager.swift index f770a3d6..21fe37cc 100644 --- a/Shared/iCloud/CloudSyncManager.swift +++ b/Shared/iCloud/CloudSyncManager.swift @@ -102,6 +102,7 @@ public enum CloudSyncError: Error, Sendable, CustomStringConvertible { case notAuthenticated case quotaExceeded case productionSchemaMissingRecordType(String) + case productionSchemaMissingQueryableIndex(String) case serverError(String) case decodingFailed(String) case unknown(String) @@ -118,6 +119,10 @@ public enum CloudSyncError: Error, Sendable, CustomStringConvertible { "CloudKit Production schema is missing record type \(recordType). " + "Deploy CloudKit schema changes to Production for " + "\(CloudSyncConstants.containerIdentifier), then try Sync Now again." + case .productionSchemaMissingQueryableIndex(let fieldName): + "CloudKit Production index for \(fieldName) is not ready yet. " + + "Deploy the queryable index for " + + "\(CloudSyncConstants.containerIdentifier), then try Sync Now again." case .serverError(let msg): "Server error: \(msg)" case .decodingFailed(let msg): @@ -133,6 +138,10 @@ public enum CloudSyncError: Error, Sendable, CustomStringConvertible { self = .productionSchemaMissingRecordType(recordType) return } + if let fieldName = Self.missingProductionQueryableIndex(in: diagnosticMessage) { + self = .productionSchemaMissingQueryableIndex(fieldName) + return + } switch ckError.code { case .networkUnavailable, .networkFailure: @@ -163,6 +172,22 @@ public enum CloudSyncError: Error, Sendable, CustomStringConvertible { return recordType.isEmpty ? nil : recordType } + public static func missingProductionQueryableIndex(in message: String) -> String? { + guard message.range( + of: "not marked queryable", + options: [.caseInsensitive]) != nil + else { return nil } + + let quotedField = #/Field\s+'([^']+)'/# + if let match = message.firstMatch(of: quotedField) { + return String(match.1) + } + if message.range(of: "recordName", options: [.caseInsensitive]) != nil { + return "recordName" + } + return nil + } + private static func diagnosticMessage(from ckError: CKError) -> String { let userInfoMessages = ckError.userInfo.values.compactMap { value -> String? in if let string = value as? String { diff --git a/docs/cloudkit-deploy-audit.md b/docs/cloudkit-deploy-audit.md index bd1f9631..33d9c9b8 100644 --- a/docs/cloudkit-deploy-audit.md +++ b/docs/cloudkit-deploy-audit.md @@ -34,6 +34,14 @@ Mac-to-iOS sync depends on these Production record types in `DeviceProviderSnapshot.deviceID` must be queryable because the Mac startup reconcile queries provider records for the current device. +`DeviceSnapshot.recordName`, `DeviceProviderSnapshot.recordName`, and +`ProviderAccountLinkage.recordName` must also be queryable. iOS full-refresh +paths issue whole-record-type CloudKit queries for those records, and +Production rejects those reads with `Field 'recordName' is not marked +queryable` when the built-in record-name index is missing. CloudKit Dashboard +labels the field `recordName`; schema exports may represent the same index as +`___recordID`. + If a release build shows `Cannot create new type DeviceSnapshot in production schema`, Production schema has not been deployed. Open CloudKit Dashboard, select `iCloud.com.columbuslabs.quotakit`, and use **Schema -> Deploy Schema From 6862511ff524170d8a9ca5aafe52e8e22eca76ac Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:56:40 -0400 Subject: [PATCH 2/4] Fix provider palette parity and branding audits --- AGENTS.md | 9 +- CodexBarMobile/CHANGELOG.md | 3 + .../CodexBarMobile/ContentView.swift | 57 ++- .../Design/SyncFreshnessChip.swift | 29 +- .../CodexBarMobile/Localizable.xcstrings | 412 +++++++++--------- .../Models/ProviderColorPalette.swift | 304 +++++-------- .../MultiAccountTabRenderingTests.swift | 10 + .../ProviderColorPaletteTests.swift | 129 +++--- .../QuotaKitProViewSmokeTests.swift | 9 +- .../034-review-findings-fix-bundle.md | 33 ++ CodexBarMobile/Research/README.md | 1 + Scripts/audit_customer_branding.py | 187 +++++--- Scripts/audit_provider_palette.py | 89 ++++ Scripts/lint.sh | 10 +- Sources/CodexBar/About.swift | 2 +- Sources/CodexBar/PreferencesAboutPane.swift | 2 +- .../AlibabaTokenPlanProviderDescriptor.swift | 2 +- .../Augment/AugmentProviderDescriptor.swift | 2 +- .../CommandCodeProviderDescriptor.swift | 2 +- .../Deepgram/DeepgramProviderDescriptor.swift | 6 +- .../Grok/GrokProviderDescriptor.swift | 2 +- .../Kimi/KimiProviderDescriptor.swift | 2 +- .../Kiro/KiroProviderDescriptor.swift | 2 +- .../Manus/ManusProviderDescriptor.swift | 2 +- .../MiMo/MiMoProviderDescriptor.swift | 2 +- .../MiniMax/MiniMaxProviderDescriptor.swift | 2 +- .../OpenCode/OpenCodeProviderDescriptor.swift | 2 +- .../OpenCodeGoProviderDescriptor.swift | 2 +- .../SyntheticProviderDescriptor.swift | 2 +- .../T3Chat/T3ChatProviderDescriptor.swift | 2 +- .../CodexBarTests/ProviderRegistryTests.swift | 19 + 31 files changed, 770 insertions(+), 567 deletions(-) create mode 100644 CodexBarMobile/Research/034-review-findings-fix-bundle.md create mode 100755 Scripts/audit_provider_palette.py diff --git a/AGENTS.md b/AGENTS.md index f55c4059..668a2686 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,8 @@ Use this sequence for feature and fix work: 3. Implementation: keep changes scoped and buildable. 4. Testing: run the narrowest useful checks, then broader checks when shared behavior changes. 5. Documentation: update changelogs, release notes, and research status as needed. -6. Commit: bump iOS build numbers only when preparing a Git commit for push. -7. Release: archive/upload only when explicitly requested. +6. Commit: keep build numbers unchanged for ordinary branch or PR commits. +7. Release: bump iOS build numbers only when preparing an actual TestFlight/App Store build; archive/upload only when explicitly requested. ## iOS Documentation Rules @@ -44,13 +44,16 @@ Do not push to `upstream`. Push QuotaKit work to `origin`, which is `https://git ## iOS Build Numbers -When preparing a pushed iOS change: +When preparing an actual iOS build for TestFlight or App Store distribution: 1. Open `CodexBarMobile/project.yml`. 2. Increment every `CURRENT_PROJECT_VERSION` value by 1. 3. Do not change `MARKETING_VERSION` unless explicitly requested. 4. Run `cd CodexBarMobile && xcodegen generate`. +Do not bump build numbers for routine local commits, review branches, PR updates, +or merges that are not being archived/uploaded as a new iOS build. + ## Localization The current iOS build pipeline requires every new user-facing `String(localized:)` key to be present in `CodexBarMobile/CodexBarMobile/Localizable.xcstrings` for all supported app locales, with every entry marked `translated`. diff --git a/CodexBarMobile/CHANGELOG.md b/CodexBarMobile/CHANGELOG.md index 4e14d095..ff78da61 100644 --- a/CodexBarMobile/CHANGELOG.md +++ b/CodexBarMobile/CHANGELOG.md @@ -17,6 +17,9 @@ current Columbus Labs product surface and recent release history. stale threshold). - Sync freshness chips now tick live, can be tapped to refresh, and keep showing refreshing or failed-refresh state after pull-to-refresh releases. +- Provider tints now mirror the Mac registry without near-collisions, stay + readable in light and dark appearances, and keep synced-time VoiceOver status + intact when the chip is tappable. - Added public Columbus Labs remote config guardrails for safe setup-link overrides, announcements, and feature kill switches. Native app changes still require a TestFlight/App Store build. diff --git a/CodexBarMobile/CodexBarMobile/ContentView.swift b/CodexBarMobile/CodexBarMobile/ContentView.swift index bc96c788..7188d418 100644 --- a/CodexBarMobile/CodexBarMobile/ContentView.swift +++ b/CodexBarMobile/CodexBarMobile/ContentView.swift @@ -218,7 +218,7 @@ struct ProviderListView: View { among: liveProviders, appVersionForProvider: { provider in // Find which device-snapshot this provider came from to - // report its CodexBar version in the §9 hint. Falls back + // report its QuotaKit version in the §9 hint. Falls back // to the merged snapshot's appVersion (the highest across // devices) — that's at least the "ceiling" of what other // Mac versions could be in play. @@ -1876,7 +1876,7 @@ private struct AboutSyncDetailView: View { if let snapshot = self.usageData.snapshot { LabeledContent("Mac App", value: snapshot.appVersion ?? String(localized: "Unknown")) // When multiple Macs sync and at least one runs an older - // CodexBar version than the highest, surface a subtle hint + // QuotaKit version than the highest, surface a subtle hint // under the Mac App row. Prompts the user to update the // older Mac so both sides can emit new-schema sync data // (perplexityCredits, loginMethod, budget, etc. — all the @@ -1951,7 +1951,11 @@ private struct AboutSyncDetailView: View { // MARK: Sync Status Section { - TimelineView(.periodic(from: .now, by: 1)) { timeline in + TimelineView(.periodic( + from: .now, + by: SyncFreshnessTimeline.cadence( + since: self.syncStatusTimelineReferenceDate))) + { timeline in HStack { self.syncStatusIcon VStack(alignment: .leading, spacing: 2) { @@ -2140,6 +2144,17 @@ private struct AboutSyncDetailView: View { return CloudSyncReader.semverLessThan(deviceVersion, latestVersion) } + private var syncStatusTimelineReferenceDate: Date? { + switch self.usageData.syncStatus { + case .synced(let lastConfirmedSync): + lastConfirmedSync + case .syncing, .error: + self.usageData.snapshot?.syncTimestamp + case .noData, .incompatibleData: + nil + } + } + private func syncStatusDetail(now: Date) -> String? { switch self.usageData.syncStatus { case .synced(let lastConfirmedSync): @@ -2644,14 +2659,14 @@ private enum MobileReleaseNotesCatalog { String(localized: "QuotaKit Pro — Free mode keeps one selected synced provider plus basic quota details, while Pro and demo mode unlock the full provider list, cost dashboard, history charts, share/export actions, advanced merge controls, and visible quota alerts."), String(localized: "Widgets and pace — QuotaKit Pro adds Home Screen and Lock Screen widgets backed only by sanitized iPhone-side snapshot data, and Usage cards now match the Mac app with deficit/reserve pace labels, projected run-out timing, and expected-usage markers."), String(localized: "Branding and setup — iOS screens, the app icon, share cards, update prompts, and Mac setup now use QuotaKit. The iPhone shares a Columbus Labs setup page for Mac installation instead of sending you straight to GitHub."), - String(localized: "Sync refresh feedback — pull-to-refresh and the synced-time chip now show a visible refreshing state until iCloud finishes, and the last-synced age keeps counting from the Mac-confirmed sync time."), + String(localized: "Sync polish — provider colors now stay distinct and readable in both appearances, and the synced-time chip keeps its status available to VoiceOver while refreshing."), String(localized: "Remote guardrails — Columbus Labs can now update safe setup links, announcements, and feature kill switches over the air while native app changes still go through TestFlight/App Store."), ]), ]), ReleaseNotesVersion( version: "1.11.0", status: "", - summary: String(localized: "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the CodexBar 0.32.4 sync."), + summary: String(localized: "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the QuotaKit Mac 0.32.4 sync."), sections: [ .init( title: String(localized: "What's New"), @@ -2666,13 +2681,13 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated."), + String(localized: "Update QuotaKit Mac to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated."), ]), ]), ReleaseNotesVersion( version: "1.10.0", status: "", - summary: String(localized: "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the CodexBar 0.31.0 sync."), + summary: String(localized: "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the QuotaKit Mac 0.31.0 sync."), sections: [ .init( title: String(localized: "What's New"), @@ -2686,13 +2701,13 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated."), + String(localized: "Update QuotaKit Mac to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated."), ]), ]), ReleaseNotesVersion( version: "1.9.0", status: "", - summary: String(localized: "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the CodexBar 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks."), + summary: String(localized: "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the QuotaKit Mac 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks."), sections: [ .init( title: String(localized: "What's New"), @@ -2705,7 +2720,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0."), + String(localized: "Update QuotaKit Mac to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0."), ]), ]), ReleaseNotesVersion( @@ -2734,7 +2749,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3."), + String(localized: "Update QuotaKit Mac to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3."), ]), ]), ReleaseNotesVersion( @@ -2756,7 +2771,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build."), + String(localized: "Update QuotaKit Mac to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build."), ]), ]), ReleaseNotesVersion( @@ -2767,7 +2782,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "What's New"), items: [ - String(localized: "11 new providers from Mac CodexBar v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page."), + String(localized: "11 new providers from QuotaKit Mac v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page."), String(localized: "Push notifications expanded to cover the 11 new providers — your iPhone now pings on their quota events the same way it does for the existing 27."), String(localized: "Claude peak-hours indicator on the Claude detail page — quick glance at whether you're inside Anthropic's published 8am-2pm ET peak window or how long until the next one starts."), String(localized: "Quota warning markers on every usage bar — tick marks at the thresholds you set on Mac (default 50% / 20% remaining) and a warning icon when you cross the most critical one. Per-provider customization on Mac flows through transparently."), @@ -2776,7 +2791,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.25.2 or later for the warning push. New providers work from 0.25.1."), + String(localized: "Update QuotaKit Mac to 0.25.2 or later for the warning push. New providers work from 0.25.1."), ]), ]), ReleaseNotesVersion( @@ -2791,12 +2806,12 @@ private enum MobileReleaseNotesCatalog { String(localized: "Claude Designs / Daily Routines / Web Sonnet usage bars on the Claude detail page; Cursor Extra budget gauge on the Cursor page."), String(localized: "Synthetic 5h / weekly tokens / search hourly labels render correctly instead of generic fallbacks."), String(localized: "Codex Pro $100 plan badge; estimated cost for newly-released models marked with *."), - String(localized: "Two Macs on different CodexBar versions during a rolling upgrade now show a single card per account."), + String(localized: "Two Macs on different QuotaKit versions during a rolling upgrade now show a single card per account."), ]), .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Requires CodexBar for Mac 0.23.4 or later for the new providers."), + String(localized: "Requires QuotaKit for Mac 0.23.4 or later for the new providers."), ]), ]), ReleaseNotesVersion( @@ -2819,7 +2834,7 @@ private enum MobileReleaseNotesCatalog { .init( title: String(localized: "Required Mac version"), items: [ - String(localized: "Update Mac CodexBar to 0.23.6 for these changes to take effect."), + String(localized: "Update QuotaKit Mac to 0.23.6 for these changes to take effect."), ]), ]), ReleaseNotesVersion( @@ -2844,7 +2859,7 @@ private enum MobileReleaseNotesCatalog { String(localized: "Codex Pro $100 plan badge — the new Pro $100 / prolite plan names from upstream v0.21 sync through and display in the account-info capsule on each Codex card."), String(localized: "Color palette extended — Abacus uses a warm brown tone, Mistral a vibrant red. Both stay distinct from existing provider colors across cards, charts, and the share image."), String(localized: "Estimated cost for newly-released models — when Mac sees a model name that isn't in its pricing table yet, it uses the closest known model's rate as a temporary estimate and marks the value with * on the Provider Detail cost card. Stops Daily Spend from quietly dropping to $0 the day a fresh model name appears."), - String(localized: "Two Macs, one card — when your two Macs are on different CodexBar versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too."), + String(localized: "Two Macs, one card — when your two Macs are on different QuotaKit versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too."), ]), .init( title: String(localized: "Under the hood"), @@ -2875,7 +2890,7 @@ private enum MobileReleaseNotesCatalog { String(localized: "Codex Pro $100 plan badge — the new Pro $100 / prolite plan names from upstream v0.21 sync through and display in the account-info capsule on each Codex card."), String(localized: "Color palette extended — Abacus uses a warm brown tone, Mistral a vibrant red. Both stay distinct from existing provider colors across cards, charts, and the share image."), String(localized: "Estimated cost for newly-released models — when Mac sees a model name that isn't in its pricing table yet, it uses the closest known model's rate as a temporary estimate and marks the value with * on the Provider Detail cost card. Stops Daily Spend from quietly dropping to $0 the day a fresh model name appears."), - String(localized: "Two Macs, one card — when your two Macs are on different CodexBar versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too."), + String(localized: "Two Macs, one card — when your two Macs are on different QuotaKit versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too."), ]), .init( title: String(localized: "Under the hood"), @@ -2926,7 +2941,7 @@ private enum MobileReleaseNotesCatalog { title: String(localized: "What's New"), items: [ String(localized: "Subscription Utilization visualization — see how much of each session / weekly / opus quota you're using, per provider and across all providers. 30-day daily bar chart in the Cost tab with Today / This Week / 14 Days / 30 Days summary cards, plus a utilization history chart on every provider detail page."), - String(localized: "Multi-Mac data merge — if you run CodexBar on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active."), + String(localized: "Multi-Mac data merge — if you run QuotaKit on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active."), String(localized: "Push notifications from Mac — when a session quota hits 0% or becomes available again on any of your Macs, your iPhone receives a notification that includes the provider name. Background App Refresh does not need to be enabled."), ]), .init( @@ -2964,7 +2979,7 @@ private enum MobileReleaseNotesCatalog { ReleaseNotesVersion( version: "1.0.0 (21)", status: "", - summary: String(localized: "The first App Store release. Works with CodexBar on Mac."), + summary: String(localized: "The first App Store release. Works with QuotaKit on Mac."), sections: [ .init( title: String(localized: "What's New"), diff --git a/CodexBarMobile/CodexBarMobile/Design/SyncFreshnessChip.swift b/CodexBarMobile/CodexBarMobile/Design/SyncFreshnessChip.swift index 85267a54..aa836b8c 100644 --- a/CodexBarMobile/CodexBarMobile/Design/SyncFreshnessChip.swift +++ b/CodexBarMobile/CodexBarMobile/Design/SyncFreshnessChip.swift @@ -120,6 +120,17 @@ enum SyncFreshnessFormatter { } } +enum SyncFreshnessTimeline { + static func cadence(since timestamp: Date?, now: Date = Date()) -> TimeInterval { + guard let timestamp else { return 60 } + let interval = max(0, now.timeIntervalSince(timestamp)) + if interval < 60 { return 1 } + if interval < 3600 { return 60 } + if interval < 86400 { return 300 } + return 3600 + } +} + struct SyncStatusChipView: View { let placement: SyncFreshnessPlacement let isDemoMode: Bool @@ -132,8 +143,22 @@ struct SyncStatusChipView: View { return false } + private var timelineReferenceDate: Date? { + switch self.syncStatus { + case .synced(let lastConfirmedSync): + lastConfirmedSync + case .syncing, .error: + self.snapshot?.syncTimestamp + case .noData, .incompatibleData: + self.snapshot?.syncTimestamp + } + } + var body: some View { - TimelineView(.periodic(from: .now, by: 1)) { timeline in + TimelineView(.periodic( + from: .now, + by: SyncFreshnessTimeline.cadence(since: self.timelineReferenceDate))) + { timeline in if let state = SyncFreshnessState.resolve( isDemoMode: self.isDemoMode, snapshot: self.snapshot, @@ -164,7 +189,7 @@ struct SyncStatusChipView: View { } .buttonStyle(.plain) .disabled(self.isRefreshing) - .accessibilityLabel(Text("Refresh synced data")) + .accessibilityHint(Text("Refresh synced data")) } else { content() } diff --git a/CodexBarMobile/CodexBarMobile/Localizable.xcstrings b/CodexBarMobile/CodexBarMobile/Localizable.xcstrings index 30404f7d..4a7af238 100644 --- a/CodexBarMobile/CodexBarMobile/Localizable.xcstrings +++ b/CodexBarMobile/CodexBarMobile/Localizable.xcstrings @@ -227,30 +227,30 @@ } } }, - "%lld synthetic providers from Mac. Toggle off in Mac CodexBar → Settings → Mobile → Debug · Mock Provider Data; iPhone updates within ~30s.": { + "%lld synthetic providers from Mac. Toggle off in QuotaKit Mac → Settings → Mobile → Debug · Mock Provider Data; iPhone updates within ~30s.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%lld synthetic providers from Mac. Toggle off in Mac CodexBar → Settings → Mobile → Debug · Mock Provider Data; iPhone updates within ~30s." + "value": "%lld synthetic providers from Mac. Toggle off in QuotaKit Mac → Settings → Mobile → Debug · Mock Provider Data; iPhone updates within ~30s." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac から %lld 個の合成プロバイダー。Mac CodexBar → 設定 → Mobile → Debug · Mock Provider Data でオフに切り替えると、iPhone は約 30 秒以内に更新されます。" + "value": "Mac から %lld 個の合成プロバイダー。QuotaKit Mac → 設定 → Mobile → Debug · Mock Provider Data でオフに切り替えると、iPhone は約 30 秒以内に更新されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%lld 个模拟 provider 来自 Mac。在 Mac CodexBar → 设置 → Mobile → Debug · Mock Provider Data 关闭后,iPhone 会在约 30 秒内更新。" + "value": "%lld 个模拟 provider 来自 Mac。在 QuotaKit Mac → 设置 → Mobile → Debug · Mock Provider Data 关闭后,iPhone 会在约 30 秒内更新。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%lld 個模擬 provider 來自 Mac。在 Mac CodexBar → 設定 → Mobile → Debug · Mock Provider Data 關閉後,iPhone 會在約 30 秒內更新。" + "value": "%lld 個模擬 provider 來自 Mac。在 QuotaKit Mac → 設定 → Mobile → Debug · Mock Provider Data 關閉後,iPhone 會在約 30 秒內更新。" } } } @@ -425,30 +425,30 @@ } } }, - "11 new providers from Mac CodexBar v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page.": { + "11 new providers from QuotaKit Mac v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "11 new providers from Mac CodexBar v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page." + "value": "11 new providers from QuotaKit Mac v0.24/v0.25 — Windsurf, Codebuff, DeepSeek, Manus, Xiaomi MiMo, Doubao, Command Code, StepFun, Crof, Venice, OpenAI API. Each renders in its own brand color across Usage / Cost / Subscription tabs and on the provider detail page." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac CodexBar v0.24/v0.25 で追加された 11 個の新プロバイダー(Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API)。Usage / Cost / Subscription タブとプロバイダー詳細ページで、それぞれのブランドカラーで表示されます。" + "value": "QuotaKit Mac v0.24/v0.25 で追加された 11 個の新プロバイダー(Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API)。Usage / Cost / Subscription タブとプロバイダー詳細ページで、それぞれのブランドカラーで表示されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Mac CodexBar v0.24/v0.25 加入的 11 个新 provider —— Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API。iOS 端 Usage / Cost / Subscription 各 tab 和 provider 详情页都按各自品牌色渲染。" + "value": "QuotaKit Mac v0.24/v0.25 加入的 11 个新 provider —— Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API。iOS 端 Usage / Cost / Subscription 各 tab 和 provider 详情页都按各自品牌色渲染。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Mac CodexBar v0.24/v0.25 加入的 11 個新 provider —— Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API。iOS 端 Usage / Cost / Subscription 各 tab 與 provider 詳情頁皆按各自品牌色渲染。" + "value": "QuotaKit Mac v0.24/v0.25 加入的 11 個新 provider —— Windsurf、Codebuff、DeepSeek、Manus、Xiaomi MiMo、Doubao、Command Code、StepFun、Crof、Venice、OpenAI API。iOS 端 Usage / Cost / Subscription 各 tab 與 provider 詳情頁皆按各自品牌色渲染。" } } } @@ -1523,58 +1523,58 @@ } } }, - "CodexBar": { + "QuotaKit": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "CodexBar" + "value": "QuotaKit" } }, "ja": { "stringUnit": { "state": "translated", - "value": "CodexBar" + "value": "QuotaKit" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "CodexBar" + "value": "QuotaKit" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "CodexBar" + "value": "QuotaKit" } } } }, - "CodexBar (Demo)": { + "QuotaKit (Demo)": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "CodexBar (Demo)" + "value": "QuotaKit (Demo)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "CodexBar(デモ)" + "value": "QuotaKit(デモ)" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "CodexBar(演示)" + "value": "QuotaKit(演示)" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "CodexBar(示範)" + "value": "QuotaKit(示範)" } } } @@ -2533,30 +2533,30 @@ } } }, - "Enable cost collection in CodexBar on your Mac to see provider spend, breakdowns, and budgets here.": { + "Enable cost collection in QuotaKit on your Mac to see provider spend, breakdowns, and budgets here.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Enable cost collection in CodexBar on your Mac to see provider spend, breakdowns, and budgets here." + "value": "Enable cost collection in QuotaKit on your Mac to see provider spend, breakdowns, and budgets here." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar でコスト収集を有効にすると、ここでプロバイダごとの支出、内訳、予算を確認できます。" + "value": "Mac の QuotaKit でコスト収集を有効にすると、ここでプロバイダごとの支出、内訳、予算を確認できます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 Mac 版 CodexBar 中启用费用收集后,这里会显示 provider 支出、细分和预算。" + "value": "在 Mac 版 QuotaKit 中启用费用收集后,这里会显示 provider 支出、细分和预算。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 Mac 版 CodexBar 中啟用費用收集後,這裡會顯示 provider 支出、細分和預算。" + "value": "在 Mac 版 QuotaKit 中啟用費用收集後,這裡會顯示 provider 支出、細分和預算。" } } } @@ -2589,30 +2589,30 @@ } } }, - "Enable providers in CodexBar on your Mac to see usage data here.": { + "Enable providers in QuotaKit on your Mac to see usage data here.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Enable providers in CodexBar on your Mac to see usage data here." + "value": "Enable providers in QuotaKit on your Mac to see usage data here." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar で provider を有効にすると、ここに使用データが表示されます。" + "value": "Mac の QuotaKit で provider を有効にすると、ここに使用データが表示されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 Mac 版 CodexBar 中启用 provider 后,这里才会显示使用数据。" + "value": "在 Mac 版 QuotaKit 中启用 provider 后,这里才会显示使用数据。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 Mac 版 CodexBar 中啟用 provider 後,這裡才會顯示使用資料。" + "value": "在 Mac 版 QuotaKit 中啟用 provider 後,這裡才會顯示使用資料。" } } } @@ -3236,30 +3236,30 @@ } } }, - "Install CodexBar on Mac": { + "Install QuotaKit on Mac": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Install CodexBar on Mac" + "value": "Install QuotaKit on Mac" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac に CodexBar をインストール" + "value": "Mac に QuotaKit をインストール" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 Mac 上安装 CodexBar" + "value": "在 Mac 上安装 QuotaKit" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 Mac 上安裝 CodexBar" + "value": "在 Mac 上安裝 QuotaKit" } } } @@ -4219,30 +4219,30 @@ } } }, - "Monitor your AI coding tool usage on iPhone.\nRequires the CodexBar Mac app.": { + "Monitor your AI coding tool usage on iPhone.\nRequires the QuotaKit Mac app.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Monitor your AI coding tool usage on iPhone.\nRequires the CodexBar Mac app." + "value": "Monitor your AI coding tool usage on iPhone.\nRequires the QuotaKit Mac app." } }, "ja": { "stringUnit": { "state": "translated", - "value": "iPhone で AI コーディングツールの使用状況を確認できます。\nCodexBar Mac App が必要です。" + "value": "iPhone で AI コーディングツールの使用状況を確認できます。\nQuotaKit Mac App が必要です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 iPhone 上查看 AI 编程工具的使用情况。\n需要安装 CodexBar Mac App。" + "value": "在 iPhone 上查看 AI 编程工具的使用情况。\n需要安装 QuotaKit Mac App。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 iPhone 上查看 AI 編程工具的使用情況。\n需要安裝 CodexBar Mac App。" + "value": "在 iPhone 上查看 AI 編程工具的使用情況。\n需要安裝 QuotaKit Mac App。" } } } @@ -4303,30 +4303,30 @@ } } }, - "Multi-Mac data merge — if you run CodexBar on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active.": { + "Multi-Mac data merge — if you run QuotaKit on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Multi-Mac data merge — if you run CodexBar on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active." + "value": "Multi-Mac data merge — if you run QuotaKit on more than one Mac, data from all of them is deduped by hour and combined on iPhone, so your iPhone charts stay consistent regardless of which Mac was last active." } }, "ja": { "stringUnit": { "state": "translated", - "value": "複数 Mac のデータ統合 — 複数の Mac で CodexBar を使用している場合、すべての Mac のデータが時間単位で重複排除され iPhone 上で統合されます。最後にアクティブだった Mac に関わらず、iPhone のチャートは一貫して表示されます。" + "value": "複数 Mac のデータ統合 — 複数の Mac で QuotaKit を使用している場合、すべての Mac のデータが時間単位で重複排除され iPhone 上で統合されます。最後にアクティブだった Mac に関わらず、iPhone のチャートは一貫して表示されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "多 Mac 数据合并 —— 如果你在多台 Mac 上使用 CodexBar,所有 Mac 的数据现在都会在 iPhone 上按小时去重后合并,不管最后活跃的是哪台 Mac,iPhone 图表都一致。" + "value": "多 Mac 数据合并 —— 如果你在多台 Mac 上使用 QuotaKit,所有 Mac 的数据现在都会在 iPhone 上按小时去重后合并,不管最后活跃的是哪台 Mac,iPhone 图表都一致。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "多 Mac 資料合併 —— 如果你在多台 Mac 上使用 CodexBar,所有 Mac 的資料現在都會在 iPhone 上按小時去重後合併,不管最後活躍的是哪台 Mac,iPhone 圖表都一致。" + "value": "多 Mac 資料合併 —— 如果你在多台 Mac 上使用 QuotaKit,所有 Mac 的資料現在都會在 iPhone 上按小時去重後合併,不管最後活躍的是哪台 Mac,iPhone 圖表都一致。" } } } @@ -4723,30 +4723,30 @@ } } }, - "No utilization data yet. Keep CodexBar running on your Mac to start recording.": { + "No utilization data yet. Keep QuotaKit running on your Mac to start recording.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No utilization data yet. Keep CodexBar running on your Mac to start recording." + "value": "No utilization data yet. Keep QuotaKit running on your Mac to start recording." } }, "ja": { "stringUnit": { "state": "translated", - "value": "利用率データがまだありません。Mac で CodexBar を起動したままにしてください。" + "value": "利用率データがまだありません。Mac で QuotaKit を起動したままにしてください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "暂无利用率数据。请保持 Mac 上的 CodexBar 运行以开始记录。" + "value": "暂无利用率数据。请保持 Mac 上的 QuotaKit 运行以开始记录。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尚無利用率資料。請保持 Mac 上的 CodexBar 運行以開始記錄。" + "value": "尚無利用率資料。請保持 Mac 上的 QuotaKit 運行以開始記錄。" } } } @@ -4975,30 +4975,30 @@ } } }, - "Open CodexBar on your Mac → Settings → turn on iCloud Sync.": { + "Open QuotaKit on your Mac → Settings → turn on iCloud Sync.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open CodexBar on your Mac → Settings → turn on iCloud Sync." + "value": "Open QuotaKit on your Mac → Settings → turn on iCloud Sync." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac で CodexBar を開き、→ Settings → iCloud Sync をオンにしてください。" + "value": "Mac で QuotaKit を開き、→ Settings → iCloud Sync をオンにしてください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 Mac 上打开 CodexBar → Settings → 打开 iCloud Sync。" + "value": "在 Mac 上打开 QuotaKit → Settings → 打开 iCloud Sync。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 Mac 上開啟 CodexBar → Settings → 打開 iCloud Sync。" + "value": "在 Mac 上開啟 QuotaKit → Settings → 打開 iCloud Sync。" } } } @@ -5510,30 +5510,30 @@ } } }, - "Please update CodexBar on Mac": { + "Please update QuotaKit on Mac": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Please update CodexBar on Mac" + "value": "Please update QuotaKit on Mac" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请在 Mac 端更新 CodexBar" + "value": "请在 Mac 端更新 QuotaKit" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請在 Mac 端更新 CodexBar" + "value": "請在 Mac 端更新 QuotaKit" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar をアップデートしてください" + "value": "Mac の QuotaKit をアップデートしてください" } } } @@ -6615,30 +6615,30 @@ } } }, - "Recognises five new providers from Mac CodexBar 0.27.0 (Grok, ElevenLabs, Deepgram, GroqCloud, LLM Proxy) with distinct brand colours, and surfaces Kiro overage usage on the Kiro card when your plan is exhausted.": { + "Recognises five new providers from QuotaKit Mac 0.27.0 (Grok, ElevenLabs, Deepgram, GroqCloud, LLM Proxy) with distinct brand colours, and surfaces Kiro overage usage on the Kiro card when your plan is exhausted.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Recognises five new providers from Mac CodexBar 0.27.0 (Grok, ElevenLabs, Deepgram, GroqCloud, LLM Proxy) with distinct brand colours, and surfaces Kiro overage usage on the Kiro card when your plan is exhausted." + "value": "Recognises five new providers from QuotaKit Mac 0.27.0 (Grok, ElevenLabs, Deepgram, GroqCloud, LLM Proxy) with distinct brand colours, and surfaces Kiro overage usage on the Kiro card when your plan is exhausted." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "识别 Mac CodexBar 0.27.0 的五个新厂商(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy),分别配以独特品牌色;当 Kiro 套餐用尽时,Kiro 卡片显示超额用量。" + "value": "识别 QuotaKit Mac 0.27.0 的五个新厂商(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy),分别配以独特品牌色;当 Kiro 套餐用尽时,Kiro 卡片显示超额用量。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "識別 Mac CodexBar 0.27.0 的五個新廠商(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy),分別配以獨特品牌色;當 Kiro 方案用盡時,Kiro 卡片顯示超額用量。" + "value": "識別 QuotaKit Mac 0.27.0 的五個新廠商(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy),分別配以獨特品牌色;當 Kiro 方案用盡時,Kiro 卡片顯示超額用量。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac CodexBar 0.27.0 で追加された 5 つの新プロバイダ(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy)をそれぞれのブランドカラーで認識し、Kiro プランが上限に達するとカード上に超過分の使用量を表示します。" + "value": "QuotaKit Mac 0.27.0 で追加された 5 つの新プロバイダ(Grok、ElevenLabs、Deepgram、GroqCloud、LLM Proxy)をそれぞれのブランドカラーで認識し、Kiro プランが上限に達するとカード上に超過分の使用量を表示します。" } } } @@ -6728,30 +6728,30 @@ } } }, - "Requires CodexBar for Mac 0.23.4 or later for the new providers.": { + "Requires QuotaKit for Mac 0.23.4 or later for the new providers.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Requires CodexBar for Mac 0.23.4 or later for the new providers." + "value": "Requires QuotaKit for Mac 0.23.4 or later for the new providers." } }, "ja": { "stringUnit": { "state": "translated", - "value": "新プロバイダーを利用するには macOS 用 CodexBar 0.23.4 以降が必要です。" + "value": "新プロバイダーを利用するには macOS 用 QuotaKit 0.23.4 以降が必要です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需 macOS CodexBar 0.23.4 或更新版本以启用新 provider 支持。" + "value": "需 macOS QuotaKit 0.23.4 或更新版本以启用新 provider 支持。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需 macOS CodexBar 0.23.4 或更新版本以啟用新 provider 支援。" + "value": "需 macOS QuotaKit 0.23.4 或更新版本以啟用新 provider 支援。" } } } @@ -7852,31 +7852,31 @@ } } }, - "Subscription Utilization and Mac→iPhone push notifications need CodexBar Mac 0.19.0 (Build 54.1.2.0) or later. Get it from github.com/ColumbusLabs/QuotaKit/releases.": { + "Subscription Utilization and Mac→iPhone push notifications need QuotaKit Mac 0.19.0 (Build 54.1.2.0) or later. Get it from github.com/ColumbusLabs/QuotaKit/releases.": { "comment": "Setup Guide upgrade-notice body. Mirrors the Release Notes Important section.", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Subscription Utilization and Mac→iPhone push notifications need CodexBar Mac 0.19.0 (Build 54.1.2.0) or later. Get it from github.com/ColumbusLabs/QuotaKit/releases." + "value": "Subscription Utilization and Mac→iPhone push notifications need QuotaKit Mac 0.19.0 (Build 54.1.2.0) or later. Get it from github.com/ColumbusLabs/QuotaKit/releases." } }, "ja": { "stringUnit": { "state": "translated", - "value": "サブスクリプション利用率と Mac→iPhone プッシュ通知には CodexBar Mac 0.19.0 (Build 54.1.2.0) 以降が必要です。github.com/ColumbusLabs/QuotaKit/releases から入手してください。" + "value": "サブスクリプション利用率と Mac→iPhone プッシュ通知には QuotaKit Mac 0.19.0 (Build 54.1.2.0) 以降が必要です。github.com/ColumbusLabs/QuotaKit/releases から入手してください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "订阅利用率和 Mac→iPhone 推送通知需要 Mac 版 CodexBar 升级到 0.19.0 (Build 54.1.2.0) 或更新版本。从 github.com/ColumbusLabs/QuotaKit/releases 下载。" + "value": "订阅利用率和 Mac→iPhone 推送通知需要 Mac 版 QuotaKit 升级到 0.19.0 (Build 54.1.2.0) 或更新版本。从 github.com/ColumbusLabs/QuotaKit/releases 下载。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "訂閱利用率和 Mac→iPhone 推送通知需要 Mac 版 CodexBar 升級到 0.19.0 (Build 54.1.2.0) 或更新版本。從 github.com/ColumbusLabs/QuotaKit/releases 下載。" + "value": "訂閱利用率和 Mac→iPhone 推送通知需要 Mac 版 QuotaKit 升級到 0.19.0 (Build 54.1.2.0) 或更新版本。從 github.com/ColumbusLabs/QuotaKit/releases 下載。" } } } @@ -8358,58 +8358,58 @@ } } }, - "The first App Store release. Works with CodexBar on Mac.": { + "The first App Store release. Works with QuotaKit on Mac.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The first App Store release. Works with CodexBar on Mac." + "value": "The first App Store release. Works with QuotaKit on Mac." } }, "ja": { "stringUnit": { "state": "translated", - "value": "初の App Store リリース。Mac 版 CodexBar と連携。" + "value": "初の App Store リリース。Mac 版 QuotaKit と連携。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "首个 App Store 正式版本,搭配 Mac 上的 CodexBar 使用。" + "value": "首个 App Store 正式版本,搭配 Mac 上的 QuotaKit 使用。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "首個 App Store 正式版本,搭配 Mac 上的 CodexBar 使用。" + "value": "首個 App Store 正式版本,搭配 Mac 上的 QuotaKit 使用。" } } } }, - "The first iPhone companion app for CodexBar with iCloud Key-Value Store sync from Mac.": { + "The first iPhone companion app for QuotaKit with iCloud Key-Value Store sync from Mac.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The first iPhone companion app for CodexBar with iCloud Key-Value Store sync from Mac." + "value": "The first iPhone companion app for QuotaKit with iCloud Key-Value Store sync from Mac." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac から iCloud Key-Value Store で同期する、CodexBar 初の iPhone コンパニオン App です。" + "value": "Mac から iCloud Key-Value Store で同期する、QuotaKit 初の iPhone コンパニオン App です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "CodexBar 首个 iPhone 配套 App,可通过 iCloud 键值存储从 Mac 同步数据。" + "value": "QuotaKit 首个 iPhone 配套 App,可通过 iCloud 键值存储从 Mac 同步数据。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "CodexBar 首個 iPhone 配套 App,可透過 iCloud 鍵值儲存從 Mac 同步資料。" + "value": "QuotaKit 首個 iPhone 配套 App,可透過 iCloud 鍵值儲存從 Mac 同步資料。" } } } @@ -8722,59 +8722,59 @@ } } }, - "Two Macs on different CodexBar versions during a rolling upgrade now show a single card per account.": { + "Two Macs on different QuotaKit versions during a rolling upgrade now show a single card per account.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Two Macs on different CodexBar versions during a rolling upgrade now show a single card per account." + "value": "Two Macs on different QuotaKit versions during a rolling upgrade now show a single card per account." } }, "ja": { "stringUnit": { "state": "translated", - "value": "2 台の Mac が異なる CodexBar バージョンで稼働しているローリングアップグレード中も、同じアカウントが 1 枚のカードにまとまります。" + "value": "2 台の Mac が異なる QuotaKit バージョンで稼働しているローリングアップグレード中も、同じアカウントが 1 枚のカードにまとまります。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "两台 Mac 跑不同 CodexBar 版本时,同一账号合并为一张卡。" + "value": "两台 Mac 跑不同 QuotaKit 版本时,同一账号合并为一张卡。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "兩台 Mac 跑不同 CodexBar 版本時,同一帳號合併為一張卡片。" + "value": "兩台 Mac 跑不同 QuotaKit 版本時,同一帳號合併為一張卡片。" } } } }, - "Two Macs, one card — when your two Macs are on different CodexBar versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too.": { + "Two Macs, one card — when your two Macs are on different QuotaKit versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Two Macs, one card — when your two Macs are on different CodexBar versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too." + "value": "Two Macs, one card — when your two Macs are on different QuotaKit versions during a rolling upgrade, your iPhone now correctly shows a single card per account rather than duplicates. Works for accounts whose email contains non-ASCII characters (café@…) too." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "双 Mac、一张卡 —— 两台 Mac 跑不同版本的 CodexBar(升级过渡期)时,iPhone 现在能把同一账号正确合并成一张卡,而不是每个 Mac 版本一张。邮箱含非 ASCII 字符(café@…)的账号也涵盖。" + "value": "双 Mac、一张卡 —— 两台 Mac 跑不同版本的 QuotaKit(升级过渡期)时,iPhone 现在能把同一账号正确合并成一张卡,而不是每个 Mac 版本一张。邮箱含非 ASCII 字符(café@…)的账号也涵盖。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "雙 Mac、一張卡 —— 兩台 Mac 跑不同版本的 CodexBar(升級過渡期)時,iPhone 現在能把同一帳號正確合併成一張卡,而不是每個 Mac 版本一張。郵箱含非 ASCII 字元(café@…)的帳號也涵蓋。" + "value": "雙 Mac、一張卡 —— 兩台 Mac 跑不同版本的 QuotaKit(升級過渡期)時,iPhone 現在能把同一帳號正確合併成一張卡,而不是每個 Mac 版本一張。郵箱含非 ASCII 字元(café@…)的帳號也涵蓋。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "2 台の Mac、1 枚のカード —— 2 台の Mac が異なるバージョンの CodexBar を実行中(アップグレード移行期)でも、iPhone は同一アカウントを 1 枚のカードに正しくまとめて表示するようになりました(バージョンごとに別々ではなく)。メールアドレスに非 ASCII 文字(café@…)が含まれるアカウントも対応します。" + "value": "2 台の Mac、1 枚のカード —— 2 台の Mac が異なるバージョンの QuotaKit を実行中(アップグレード移行期)でも、iPhone は同一アカウントを 1 枚のカードに正しくまとめて表示するようになりました(バージョンごとに別々ではなく)。メールアドレスに非 ASCII 文字(café@…)が含まれるアカウントも対応します。" } } } @@ -8891,229 +8891,229 @@ } } }, - "Update Mac CodexBar to 0.23.6 (latest) for both fixes to take effect.": { + "Update QuotaKit Mac to 0.23.6 (latest) for both fixes to take effect.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.23.6 (latest) for both fixes to take effect." + "value": "Update QuotaKit Mac to 0.23.6 (latest) for both fixes to take effect." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar を 0.23.6(最新)に更新すると、両方の修正が有効になります。" + "value": "Mac の QuotaKit を 0.23.6(最新)に更新すると、両方の修正が有効になります。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "把 Mac 端的 CodexBar 升级到 0.23.6(最新版),两项修复都会生效。" + "value": "把 Mac 端的 QuotaKit 升级到 0.23.6(最新版),两项修复都会生效。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "把 Mac 端的 CodexBar 升級到 0.23.6(最新版),兩項修復都會生效。" + "value": "把 Mac 端的 QuotaKit 升級到 0.23.6(最新版),兩項修復都會生效。" } } } }, - "Update Mac CodexBar to 0.23.6 (latest) for both the new visual treatment and the fixes to take effect.": { + "Update QuotaKit Mac to 0.23.6 (latest) for both the new visual treatment and the fixes to take effect.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.23.6 (latest) for both the new visual treatment and the fixes to take effect." + "value": "Update QuotaKit Mac to 0.23.6 (latest) for both the new visual treatment and the fixes to take effect." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar を 0.23.6(最新)に更新すると、新しい視覚処理と修正の両方が有効になります。" + "value": "Mac の QuotaKit を 0.23.6(最新)に更新すると、新しい視覚処理と修正の両方が有効になります。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "把 Mac 端的 CodexBar 升级到 0.23.6(最新版),新的视觉效果和修复都会生效。" + "value": "把 Mac 端的 QuotaKit 升级到 0.23.6(最新版),新的视觉效果和修复都会生效。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "把 Mac 端的 CodexBar 升級到 0.23.6(最新版),新的視覺效果和修復都會生效。" + "value": "把 Mac 端的 QuotaKit 升級到 0.23.6(最新版),新的視覺效果和修復都會生效。" } } } }, - "Update Mac CodexBar to 0.23.6 for these changes to take effect.": { + "Update QuotaKit Mac to 0.23.6 for these changes to take effect.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.23.6 for these changes to take effect." + "value": "Update QuotaKit Mac to 0.23.6 for these changes to take effect." } }, "ja": { "stringUnit": { "state": "translated", - "value": "これらの変更を有効にするには、Mac の CodexBar を 0.23.6 に更新してください。" + "value": "これらの変更を有効にするには、Mac の QuotaKit を 0.23.6 に更新してください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需搭配 Mac CodexBar 0.23.6。" + "value": "需搭配 QuotaKit Mac 0.23.6。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需搭配 Mac CodexBar 0.23.6。" + "value": "需搭配 QuotaKit Mac 0.23.6。" } } } }, - "Update Mac CodexBar to 0.25.1 or later for the new providers and push notifications.": { + "Update QuotaKit Mac to 0.25.1 or later for the new providers and push notifications.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.25.1 or later for the new providers and push notifications." + "value": "Update QuotaKit Mac to 0.25.1 or later for the new providers and push notifications." } }, "ja": { "stringUnit": { "state": "translated", - "value": "新プロバイダーとプッシュ通知の利用には macOS 用 CodexBar 0.25.1 以降が必要です。" + "value": "新プロバイダーとプッシュ通知の利用には macOS 用 QuotaKit 0.25.1 以降が必要です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "新 provider 与推送通知需更新 Mac CodexBar 至 0.25.1 或更新版本。" + "value": "新 provider 与推送通知需更新 QuotaKit Mac 至 0.25.1 或更新版本。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "新 provider 與推送通知需更新 Mac CodexBar 至 0.25.1 或更新版本。" + "value": "新 provider 與推送通知需更新 QuotaKit Mac 至 0.25.1 或更新版本。" } } } }, - "Update Mac CodexBar to 0.25.2 or later for the warning push. New providers work from 0.25.1.": { + "Update QuotaKit Mac to 0.25.2 or later for the warning push. New providers work from 0.25.1.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.25.2 or later for the warning push. New providers work from 0.25.1." + "value": "Update QuotaKit Mac to 0.25.2 or later for the warning push. New providers work from 0.25.1." } }, "ja": { "stringUnit": { "state": "translated", - "value": "警告プッシュ通知の利用には macOS 用 CodexBar 0.25.2 以降が必要です。新プロバイダーは 0.25.1 から利用できます。" + "value": "警告プッシュ通知の利用には macOS 用 QuotaKit 0.25.2 以降が必要です。新プロバイダーは 0.25.1 から利用できます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "警告推送需更新 Mac CodexBar 至 0.25.2 或更新版本;新 provider 从 0.25.1 起可用。" + "value": "警告推送需更新 QuotaKit Mac 至 0.25.2 或更新版本;新 provider 从 0.25.1 起可用。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "警告推送需更新 Mac CodexBar 至 0.25.2 或更新版本;新 provider 自 0.25.1 起可用。" + "value": "警告推送需更新 QuotaKit Mac 至 0.25.2 或更新版本;新 provider 自 0.25.1 起可用。" } } } }, - "Update Mac CodexBar to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build.": { + "Update QuotaKit Mac to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build." + "value": "Update QuotaKit Mac to 0.26.1 (fork build 63.2 or later). iPhone 1.7.0 is also forward-compatible with Mac 0.25.2 — new cards just stay hidden until Mac is on the new build." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请将 Mac 端 CodexBar 升级到 0.26.1(fork build 63.2 或更新)。iPhone 1.7.0 与 Mac 0.25.2 也保持向前兼容 —— 新卡片在 Mac 升级前会自动隐藏。" + "value": "请将 Mac 端 QuotaKit 升级到 0.26.1(fork build 63.2 或更新)。iPhone 1.7.0 与 Mac 0.25.2 也保持向前兼容 —— 新卡片在 Mac 升级前会自动隐藏。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請將 Mac 端 CodexBar 升級到 0.26.1(fork build 63.2 或更新)。iPhone 1.7.0 與 Mac 0.25.2 也保持向前相容 —— 新卡片在 Mac 升級前會自動隱藏。" + "value": "請將 Mac 端 QuotaKit 升級到 0.26.1(fork build 63.2 或更新)。iPhone 1.7.0 與 Mac 0.25.2 也保持向前相容 —— 新卡片在 Mac 升級前會自動隱藏。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac の CodexBar を 0.26.1(fork build 63.2 以降)にアップデートしてください。iPhone 1.7.0 は Mac 0.25.2 とも前方互換性があり、新しいカードは Mac がアップデートされるまで非表示になります。" + "value": "Mac の QuotaKit を 0.26.1(fork build 63.2 以降)にアップデートしてください。iPhone 1.7.0 は Mac 0.25.2 とも前方互換性があり、新しいカードは Mac がアップデートされるまで非表示になります。" } } } }, - "Update Mac CodexBar to 0.27.0 (fork build 65.1 or later) for the new dedicated cards and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new cards just stay hidden until Mac is on the new build.": { + "Update QuotaKit Mac to 0.27.0 (fork build 65.1 or later) for the new dedicated cards and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new cards just stay hidden until Mac is on the new build.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.27.0 (fork build 65.1 or later) for the new dedicated cards and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new cards just stay hidden until Mac is on the new build." + "value": "Update QuotaKit Mac to 0.27.0 (fork build 65.1 or later) for the new dedicated cards and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new cards just stay hidden until Mac is on the new build." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请将 Mac CodexBar 更新到 0.27.0(fork 构建 65.1 及更新)以启用专属卡片和 Kiro 超额数据。iPhone 1.8.0 仍向前兼容 Mac 0.26.x —— 新卡片在 Mac 升级前保持隐藏。" + "value": "请将 QuotaKit Mac 更新到 0.27.0(fork 构建 65.1 及更新)以启用专属卡片和 Kiro 超额数据。iPhone 1.8.0 仍向前兼容 Mac 0.26.x —— 新卡片在 Mac 升级前保持隐藏。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請將 Mac CodexBar 更新到 0.27.0(fork 建置 65.1 及更新)以啟用專屬卡片和 Kiro 超額資料。iPhone 1.8.0 仍向前相容 Mac 0.26.x —— 新卡片在 Mac 升級前保持隱藏。" + "value": "請將 QuotaKit Mac 更新到 0.27.0(fork 建置 65.1 及更新)以啟用專屬卡片和 Kiro 超額資料。iPhone 1.8.0 仍向前相容 Mac 0.26.x —— 新卡片在 Mac 升級前保持隱藏。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しい専用カードと Kiro 超過分データを利用するには、Mac CodexBar を 0.27.0(fork ビルド 65.1 以上)に更新してください。iPhone 1.8.0 は Mac 0.26.x との上方互換性を保ち、Mac が新ビルドになるまで新カードは非表示のままです。" + "value": "新しい専用カードと Kiro 超過分データを利用するには、QuotaKit Mac を 0.27.0(fork ビルド 65.1 以上)に更新してください。iPhone 1.8.0 は Mac 0.26.x との上方互換性を保ち、Mac が新ビルドになるまで新カードは非表示のままです。" } } } }, - "Update Mac CodexBar to 0.27.0 (fork build 65.1 or later) for the new providers and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new fields just stay hidden until Mac is on the new build.": { + "Update QuotaKit Mac to 0.27.0 (fork build 65.1 or later) for the new providers and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new fields just stay hidden until Mac is on the new build.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.27.0 (fork build 65.1 or later) for the new providers and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new fields just stay hidden until Mac is on the new build." + "value": "Update QuotaKit Mac to 0.27.0 (fork build 65.1 or later) for the new providers and Kiro overage data. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x — new fields just stay hidden until Mac is on the new build." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请将 Mac CodexBar 更新到 0.27.0(fork 构建 65.1 及更新)以启用新厂商及 Kiro 超额数据。iPhone 1.8.0 仍向前兼容 Mac 0.26.x —— 新字段在 Mac 升级前保持隐藏。" + "value": "请将 QuotaKit Mac 更新到 0.27.0(fork 构建 65.1 及更新)以启用新厂商及 Kiro 超额数据。iPhone 1.8.0 仍向前兼容 Mac 0.26.x —— 新字段在 Mac 升级前保持隐藏。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請將 Mac CodexBar 更新到 0.27.0(fork 建置 65.1 及更新)以啟用新廠商及 Kiro 超額資料。iPhone 1.8.0 仍向前相容 Mac 0.26.x —— 新欄位在 Mac 升級前保持隱藏。" + "value": "請將 QuotaKit Mac 更新到 0.27.0(fork 建置 65.1 及更新)以啟用新廠商及 Kiro 超額資料。iPhone 1.8.0 仍向前相容 Mac 0.26.x —— 新欄位在 Mac 升級前保持隱藏。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しいプロバイダと Kiro の超過分データを利用するには、Mac CodexBar を 0.27.0(fork ビルド 65.1 以上)に更新してください。iPhone 1.8.0 は Mac 0.26.x との上方互換性を保ち、Mac が新ビルドになるまで新フィールドは非表示のままです。" + "value": "新しいプロバイダと Kiro の超過分データを利用するには、QuotaKit Mac を 0.27.0(fork ビルド 65.1 以上)に更新してください。iPhone 1.8.0 は Mac 0.26.x との上方互換性を保ち、Mac が新ビルドになるまで新フィールドは非表示のままです。" } } } @@ -9539,30 +9539,30 @@ } } }, - "Walk through how CodexBar syncs from Mac to iPhone": { + "Walk through how QuotaKit syncs from Mac to iPhone": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Walk through how CodexBar syncs from Mac to iPhone" + "value": "Walk through how QuotaKit syncs from Mac to iPhone" } }, "ja": { "stringUnit": { "state": "translated", - "value": "CodexBar が Mac から iPhone にどう同期するかを案内します" + "value": "QuotaKit が Mac から iPhone にどう同期するかを案内します" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "带你了解 CodexBar 如何从 Mac 同步到 iPhone" + "value": "带你了解 QuotaKit 如何从 Mac 同步到 iPhone" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "帶你了解 CodexBar 如何從 Mac 同步到 iPhone" + "value": "帶你了解 QuotaKit 如何從 Mac 同步到 iPhone" } } } @@ -9595,30 +9595,30 @@ } } }, - "Welcome to CodexBar": { + "Welcome to QuotaKit": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Welcome to CodexBar" + "value": "Welcome to QuotaKit" } }, "ja": { "stringUnit": { "state": "translated", - "value": "CodexBar へようこそ" + "value": "QuotaKit へようこそ" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "欢迎使用 CodexBar" + "value": "欢迎使用 QuotaKit" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "歡迎使用 CodexBar" + "value": "歡迎使用 QuotaKit" } } } @@ -9737,30 +9737,30 @@ } } }, - "Your Mac is using legacy sync. Update CodexBar on Mac to unlock CloudKit multi-device sync.": { + "Your Mac is using legacy sync. Update QuotaKit on Mac to unlock CloudKit multi-device sync.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Your Mac is using legacy sync. Update CodexBar on Mac to unlock CloudKit multi-device sync." + "value": "Your Mac is using legacy sync. Update QuotaKit on Mac to unlock CloudKit multi-device sync." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac は旧式の同期方式を使用しています。CodexBar Mac アプリを更新して CloudKit マルチデバイス同期を有効にしてください。" + "value": "Mac は旧式の同期方式を使用しています。QuotaKit Mac アプリを更新して CloudKit マルチデバイス同期を有効にしてください。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "你的 Mac 正在使用旧版同步。更新 Mac 端 CodexBar 以启用 CloudKit 多设备同步。" + "value": "你的 Mac 正在使用旧版同步。更新 Mac 端 QuotaKit 以启用 CloudKit 多设备同步。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "你的 Mac 正在使用舊版同步。更新 Mac 端 CodexBar 以啟用 CloudKit 多裝置同步。" + "value": "你的 Mac 正在使用舊版同步。更新 Mac 端 QuotaKit 以啟用 CloudKit 多裝置同步。" } } } @@ -10942,7 +10942,7 @@ } }, "linkage-prompt-detail": { - "comment": "Inline prompt detail when iOS can't determine the older Mac's CodexBar version.", + "comment": "Inline prompt detail when iOS can't determine the older Mac's QuotaKit version.", "localizations": { "en": { "stringUnit": { @@ -10971,30 +10971,30 @@ } }, "linkage-prompt-detail-with-version": { - "comment": "Inline prompt detail when iOS knows the older Mac's CodexBar version. %@ = CodexBar version string (e.g. '0.23.6').", + "comment": "Inline prompt detail when iOS knows the older Mac's QuotaKit version. %@ = QuotaKit version string (e.g. '0.23.6').", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The other Mac (CodexBar %@) is on an older version that doesn't share account identifiers, so iOS can't auto-link the cards. Confirm if it's the same login. Updating the other Mac to the latest CodexBar makes this automatic." + "value": "The other Mac (QuotaKit %@) is on an older version that doesn't share account identifiers, so iOS can't auto-link the cards. Confirm if it's the same login. Updating the other Mac to the latest QuotaKit makes this automatic." } }, "ja": { "stringUnit": { "state": "translated", - "value": "もう一方の Mac (CodexBar %@) は古いバージョンでアカウント識別子を共有していないため、iOS が自動でリンクできません。同じログインの場合は確認してください。もう一方の Mac を最新の CodexBar に更新すると自動的に統合されます。" + "value": "もう一方の Mac (QuotaKit %@) は古いバージョンでアカウント識別子を共有していないため、iOS が自動でリンクできません。同じログインの場合は確認してください。もう一方の Mac を最新の QuotaKit に更新すると自動的に統合されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "另一台 Mac(CodexBar %@)版本较旧,不会共享账号标识,iOS 无法自动合并。如果是同一个账号请确认。把另一台 Mac 升级到最新版本后会自动识别。" + "value": "另一台 Mac(QuotaKit %@)版本较旧,不会共享账号标识,iOS 无法自动合并。如果是同一个账号请确认。把另一台 Mac 升级到最新版本后会自动识别。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "另一台 Mac(CodexBar %@)版本較舊,不會共享帳號識別碼,iOS 無法自動合併。如果是同一個帳號請確認。把另一台 Mac 升級到最新版本後會自動辨識。" + "value": "另一台 Mac(QuotaKit %@)版本較舊,不會共享帳號識別碼,iOS 無法自動合併。如果是同一個帳號請確認。把另一台 Mac 升級到最新版本後會自動辨識。" } } } @@ -12653,30 +12653,30 @@ } } }, - "Update Mac CodexBar to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3.": { + "Update QuotaKit Mac to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3." + "value": "Update QuotaKit Mac to 0.27.0 (fork build 65.3 or later) for the full v0.27 surface including the quota account identity push title and Codex workspace badge. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 / 65.2 — newer tiles just stay hidden / fall back to the older title format until Mac is on 65.3." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请将 Mac 端 CodexBar 升级到 0.27.0(fork 构建号 65.3 或更高),以启用完整的 v0.27 功能,包括带账号身份的额度推送标题和 Codex 工作区徽章。iPhone 1.8.0 同时向后兼容 Mac 0.26.x 和 65.1 / 65.2 —— 新增卡片在 Mac 升级到 65.3 之前会自动隐藏 / 回退到旧标题格式。" + "value": "请将 Mac 端 QuotaKit 升级到 0.27.0(fork 构建号 65.3 或更高),以启用完整的 v0.27 功能,包括带账号身份的额度推送标题和 Codex 工作区徽章。iPhone 1.8.0 同时向后兼容 Mac 0.26.x 和 65.1 / 65.2 —— 新增卡片在 Mac 升级到 65.3 之前会自动隐藏 / 回退到旧标题格式。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請將 Mac 端 CodexBar 升級到 0.27.0(fork 建置號 65.3 或更高),以啟用完整的 v0.27 功能,包括帶帳號身份的額度推送標題和 Codex 工作區徽章。iPhone 1.8.0 同時向後相容 Mac 0.26.x 和 65.1 / 65.2 —— 新增卡片在 Mac 升級到 65.3 之前會自動隱藏 / 回退到舊標題格式。" + "value": "請將 Mac 端 QuotaKit 升級到 0.27.0(fork 建置號 65.3 或更高),以啟用完整的 v0.27 功能,包括帶帳號身份的額度推送標題和 Codex 工作區徽章。iPhone 1.8.0 同時向後相容 Mac 0.26.x 和 65.1 / 65.2 —— 新增卡片在 Mac 升級到 65.3 之前會自動隱藏 / 回退到舊標題格式。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "完全な v0.27 機能 (アカウント識別子付きの利用量プッシュタイトルや Codex ワークスペースバッジを含む) を有効化するには、Mac CodexBar を 0.27.0 (フォークビルド 65.3 以降) にアップデートしてください。iPhone 1.8.0 は Mac 0.26.x および 65.1 / 65.2 とも引き続き互換性があり、新しいタイルは Mac が 65.3 になるまで自動的に非表示 / 古いタイトル形式にフォールバックします。" + "value": "完全な v0.27 機能 (アカウント識別子付きの利用量プッシュタイトルや Codex ワークスペースバッジを含む) を有効化するには、QuotaKit Mac を 0.27.0 (フォークビルド 65.3 以降) にアップデートしてください。iPhone 1.8.0 は Mac 0.26.x および 65.1 / 65.2 とも引き続き互換性があり、新しいタイルは Mac が 65.3 になるまで自動的に非表示 / 古いタイトル形式にフォールバックします。" } } } @@ -12821,30 +12821,30 @@ } } }, - "Update Mac CodexBar to 0.27.0 (fork build 65.2 or later) for the new dedicated cards plus the v0.27 existing-provider extensions. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 — new tiles just stay hidden until Mac is on 65.2.": { + "Update QuotaKit Mac to 0.27.0 (fork build 65.2 or later) for the new dedicated cards plus the v0.27 existing-provider extensions. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 — new tiles just stay hidden until Mac is on 65.2.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.27.0 (fork build 65.2 or later) for the new dedicated cards plus the v0.27 existing-provider extensions. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 — new tiles just stay hidden until Mac is on 65.2." + "value": "Update QuotaKit Mac to 0.27.0 (fork build 65.2 or later) for the new dedicated cards plus the v0.27 existing-provider extensions. iPhone 1.8.0 also remains forward-compatible with Mac 0.26.x and 65.1 — new tiles just stay hidden until Mac is on 65.2." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请将 Mac 端 CodexBar 升级到 0.27.0(fork 构建号 65.2 或更高),以启用新的专属卡片和 v0.27 现有 provider 扩展。iPhone 1.8.0 同时向后兼容 Mac 0.26.x 和 65.1 —— 新增卡片在 Mac 升级到 65.2 之前会自动隐藏。" + "value": "请将 Mac 端 QuotaKit 升级到 0.27.0(fork 构建号 65.2 或更高),以启用新的专属卡片和 v0.27 现有 provider 扩展。iPhone 1.8.0 同时向后兼容 Mac 0.26.x 和 65.1 —— 新增卡片在 Mac 升级到 65.2 之前会自动隐藏。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請將 Mac 端 CodexBar 升級到 0.27.0(fork 建置號 65.2 或更高),以啟用新的專屬卡片和 v0.27 現有 provider 擴充。iPhone 1.8.0 同時向後相容 Mac 0.26.x 和 65.1 —— 新增卡片在 Mac 升級到 65.2 之前會自動隱藏。" + "value": "請將 Mac 端 QuotaKit 升級到 0.27.0(fork 建置號 65.2 或更高),以啟用新的專屬卡片和 v0.27 現有 provider 擴充。iPhone 1.8.0 同時向後相容 Mac 0.26.x 和 65.1 —— 新增卡片在 Mac 升級到 65.2 之前會自動隱藏。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しい専用カードと v0.27 既存プロバイダ拡張を有効にするため、Mac CodexBar を 0.27.0 (フォークビルド 65.2 以降) にアップデートしてください。iPhone 1.8.0 は Mac 0.26.x および 65.1 とも引き続き互換性があり、新しいタイルは Mac が 65.2 になるまで自動的に非表示になります。" + "value": "新しい専用カードと v0.27 既存プロバイダ拡張を有効にするため、QuotaKit Mac を 0.27.0 (フォークビルド 65.2 以降) にアップデートしてください。iPhone 1.8.0 は Mac 0.26.x および 65.1 とも引き続き互換性があり、新しいタイルは Mac が 65.2 になるまで自動的に非表示になります。" } } } @@ -12961,30 +12961,30 @@ } } }, - "Update Mac CodexBar to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0.": { + "Update QuotaKit Mac to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0." + "value": "Update QuotaKit Mac to 0.29.0 (fork build 68.1 or later) to see the three new providers. iPhone 1.9.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is on 0.29.0." } }, "ja": { "stringUnit": { "state": "translated", - "value": "3 つの新しいプロバイダーを表示するには、Mac の CodexBar を 0.29.0(fork build 68.1 以降)に更新してください。iPhone 1.9.0 は古い Mac ビルドと前方互換性があり、Mac が 0.29.0 になるまで新しいカードは非表示のままです。" + "value": "3 つの新しいプロバイダーを表示するには、Mac の QuotaKit を 0.29.0(fork build 68.1 以降)に更新してください。iPhone 1.9.0 は古い Mac ビルドと前方互換性があり、Mac が 0.29.0 になるまで新しいカードは非表示のままです。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "将 Mac 端 CodexBar 更新到 0.29.0(fork build 68.1 或更新)才能看到这三个新 provider。iPhone 1.9.0 与旧版 Mac 保持向前兼容 —— 在 Mac 升级到 0.29.0 之前,新卡片只是保持隐藏。" + "value": "将 Mac 端 QuotaKit 更新到 0.29.0(fork build 68.1 或更新)才能看到这三个新 provider。iPhone 1.9.0 与旧版 Mac 保持向前兼容 —— 在 Mac 升级到 0.29.0 之前,新卡片只是保持隐藏。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "將 Mac 端 CodexBar 更新到 0.29.0(fork build 68.1 或更新)才能看到這三個新 provider。iPhone 1.9.0 與舊版 Mac 保持向前相容 —— 在 Mac 升級到 0.29.0 之前,新卡片只是保持隱藏。" + "value": "將 Mac 端 QuotaKit 更新到 0.29.0(fork build 68.1 或更新)才能看到這三個新 provider。iPhone 1.9.0 與舊版 Mac 保持向前相容 —— 在 Mac 升級到 0.29.0 之前,新卡片只是保持隱藏。" } } } @@ -13409,30 +13409,30 @@ } } }, - "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the CodexBar 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks.": { + "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the QuotaKit Mac 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the CodexBar 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks." + "value": "Three new providers (Azure OpenAI, Alibaba Token Plan, T3 Chat) from the QuotaKit Mac 0.29.0 sync — plus richer detail across many providers: the iPhone now surfaces more of what your Mac already tracks." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "来自 CodexBar 0.29.0 同步的三个新 provider(Azure OpenAI、Alibaba Token Plan、T3 Chat)—— 外加多个 provider 的更丰富详情:iPhone 现在能展示更多 Mac 已经在追踪的数据。" + "value": "来自 QuotaKit 0.29.0 同步的三个新 provider(Azure OpenAI、Alibaba Token Plan、T3 Chat)—— 外加多个 provider 的更丰富详情:iPhone 现在能展示更多 Mac 已经在追踪的数据。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "來自 CodexBar 0.29.0 同步的三個新 provider(Azure OpenAI、Alibaba Token Plan、T3 Chat)—— 外加多個 provider 的更豐富詳情:iPhone 現在能展示更多 Mac 已經在追蹤的資料。" + "value": "來自 QuotaKit 0.29.0 同步的三個新 provider(Azure OpenAI、Alibaba Token Plan、T3 Chat)—— 外加多個 provider 的更豐富詳情:iPhone 現在能展示更多 Mac 已經在追蹤的資料。" } }, "ja": { "stringUnit": { "state": "translated", - "value": "CodexBar 0.29.0 同期による 3 つの新しいプロバイダー(Azure OpenAI、Alibaba Token Plan、T3 Chat)に加え、多くのプロバイダーでより詳しい情報を表示します。iPhone が Mac で追跡中のデータをより多く表示するようになりました。" + "value": "QuotaKit 0.29.0 同期による 3 つの新しいプロバイダー(Azure OpenAI、Alibaba Token Plan、T3 Chat)に加え、多くのプロバイダーでより詳しい情報を表示します。iPhone が Mac で追跡中のデータをより多く表示するようになりました。" } } } @@ -14026,30 +14026,30 @@ } } }, - "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the CodexBar 0.31.0 sync.": { + "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the QuotaKit Mac 0.31.0 sync.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the CodexBar 0.31.0 sync." + "value": "DeepSeek web-session usage and cost on your iPhone, Codex Spark and Antigravity per-model quota lanes synced through, and cost cards that show request counts in the right currency — from the QuotaKit Mac 0.31.0 sync." } }, "ja": { "stringUnit": { "state": "translated", - "value": "DeepSeek の Web セッション使用状況とコストを iPhone で確認。Codex Spark と Antigravity のモデル別クォータが同期され、コストカードはリクエスト数を正しい通貨で表示します — CodexBar 0.31.0 の同期による更新です。" + "value": "DeepSeek の Web セッション使用状況とコストを iPhone で確認。Codex Spark と Antigravity のモデル別クォータが同期され、コストカードはリクエスト数を正しい通貨で表示します — QuotaKit 0.31.0 の同期による更新です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "DeepSeek 网页会话的用量与成本现可在 iPhone 上查看,Codex Spark 与 Antigravity 的分模型配额通道同步透传,成本卡按正确币种显示请求数 —— 来自 CodexBar 0.31.0 同步。" + "value": "DeepSeek 网页会话的用量与成本现可在 iPhone 上查看,Codex Spark 与 Antigravity 的分模型配额通道同步透传,成本卡按正确币种显示请求数 —— 来自 QuotaKit 0.31.0 同步。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "DeepSeek 網頁工作階段的用量與成本現可在 iPhone 上檢視,Codex Spark 與 Antigravity 的分模型配額通道同步透傳,成本卡按正確幣別顯示請求數 —— 來自 CodexBar 0.31.0 同步。" + "value": "DeepSeek 網頁工作階段的用量與成本現可在 iPhone 上檢視,Codex Spark 與 Antigravity 的分模型配額通道同步透傳,成本卡按正確幣別顯示請求數 —— 來自 QuotaKit 0.31.0 同步。" } } } @@ -14194,58 +14194,58 @@ } } }, - "Update Mac CodexBar to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated.": { + "Update QuotaKit Mac to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated." + "value": "Update QuotaKit Mac to 0.31.0 (fork build 73.2 or later) to surface the DeepSeek card and the Codex Spark / Antigravity lanes. iPhone 1.10.0 stays forward-compatible with older Mac builds — the new cards simply stay hidden until Mac is updated." } }, "ja": { "stringUnit": { "state": "translated", - "value": "DeepSeek カードと Codex Spark/Antigravity レーンを表示するには、Mac 版 CodexBar を 0.31.0(fork build 73.2 以降)に更新してください。iPhone 1.10.0 は古い Mac ビルドとも前方互換で、新しいカードは Mac を更新するまで非表示のままです。" + "value": "DeepSeek カードと Codex Spark/Antigravity レーンを表示するには、Mac 版 QuotaKit を 0.31.0(fork build 73.2 以降)に更新してください。iPhone 1.10.0 は古い Mac ビルドとも前方互換で、新しいカードは Mac を更新するまで非表示のままです。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "将 Mac 版 CodexBar 更新到 0.31.0(fork build 73.2 或更高)即可显示 DeepSeek 卡片以及 Codex Spark / Antigravity 通道。iPhone 1.10.0 仍向前兼容旧版 Mac —— 新卡片会先隐藏,待 Mac 更新后自动出现。" + "value": "将 Mac 版 QuotaKit 更新到 0.31.0(fork build 73.2 或更高)即可显示 DeepSeek 卡片以及 Codex Spark / Antigravity 通道。iPhone 1.10.0 仍向前兼容旧版 Mac —— 新卡片会先隐藏,待 Mac 更新后自动出现。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "將 Mac 版 CodexBar 更新到 0.31.0(fork build 73.2 或更高)即可顯示 DeepSeek 卡片以及 Codex Spark / Antigravity 通道。iPhone 1.10.0 仍向前相容舊版 Mac —— 新卡片會先隱藏,待 Mac 更新後自動出現。" + "value": "將 Mac 版 QuotaKit 更新到 0.31.0(fork build 73.2 或更高)即可顯示 DeepSeek 卡片以及 Codex Spark / Antigravity 通道。iPhone 1.10.0 仍向前相容舊版 Mac —— 新卡片會先隱藏,待 Mac 更新後自動出現。" } } } }, - "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the CodexBar 0.32.4 sync.": { + "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the QuotaKit Mac 0.32.4 sync.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the CodexBar 0.32.4 sync." + "value": "Quieter, more accurate provider data synced from your Mac — Antigravity quota rows without the noise, correct Copilot usage on zero-entitlement plans, fixed Augment parsing, and steadier Claude readings — from the QuotaKit Mac 0.32.4 sync." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac から同期される provider データがより静かで正確に — Antigravity のクォータ行からノイズを除去、zero-entitlement プランでの Copilot 使用率を修正、Augment の解析を修正、Claude の表示も安定。CodexBar 0.32.4 の同期による更新です。" + "value": "Mac から同期される provider データがより静かで正確に — Antigravity のクォータ行からノイズを除去、zero-entitlement プランでの Copilot 使用率を修正、Augment の解析を修正、Claude の表示も安定。QuotaKit 0.32.4 の同期による更新です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "从 Mac 同步来的 provider 数据更干净、更准确 —— Antigravity 配额行去除噪声、修正 zero-entitlement 套餐的 Copilot 用量、修复 Augment 解析、Claude 读数更稳。来自 CodexBar 0.32.4 同步。" + "value": "从 Mac 同步来的 provider 数据更干净、更准确 —— Antigravity 配额行去除噪声、修正 zero-entitlement 套餐的 Copilot 用量、修复 Augment 解析、Claude 读数更稳。来自 QuotaKit 0.32.4 同步。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "從 Mac 同步來的 provider 資料更乾淨、更準確 —— Antigravity 配額行去除雜訊、修正 zero-entitlement 方案的 Copilot 用量、修復 Augment 解析、Claude 讀數更穩。來自 CodexBar 0.32.4 同步。" + "value": "從 Mac 同步來的 provider 資料更乾淨、更準確 —— Antigravity 配額行去除雜訊、修正 zero-entitlement 方案的 Copilot 用量、修復 Augment 解析、Claude 讀數更穩。來自 QuotaKit 0.32.4 同步。" } } } @@ -14390,30 +14390,30 @@ } } }, - "Update Mac CodexBar to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated.": { + "Update QuotaKit Mac to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Update Mac CodexBar to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated." + "value": "Update QuotaKit Mac to 0.32.4 (fork build 79.1 or later). iPhone 1.11.0 stays forward-compatible with older Mac builds — these refinements simply arrive once Mac is updated." } }, "ja": { "stringUnit": { "state": "translated", - "value": "Mac 版 CodexBar を 0.32.4(fork build 79.1 以降)に更新してください。iPhone 1.11.0 は古い Mac ビルドとも前方互換で、これらの改善は Mac を更新すると反映されます。" + "value": "Mac 版 QuotaKit を 0.32.4(fork build 79.1 以降)に更新してください。iPhone 1.11.0 は古い Mac ビルドとも前方互換で、これらの改善は Mac を更新すると反映されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "将 Mac 版 CodexBar 更新到 0.32.4(fork build 79.1 或更高)。iPhone 1.11.0 仍向前兼容旧版 Mac —— 这些改进会在 Mac 更新后到达。" + "value": "将 Mac 版 QuotaKit 更新到 0.32.4(fork build 79.1 或更高)。iPhone 1.11.0 仍向前兼容旧版 Mac —— 这些改进会在 Mac 更新后到达。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "將 Mac 版 CodexBar 更新到 0.32.4(fork build 79.1 或更高)。iPhone 1.11.0 仍向前相容舊版 Mac —— 這些改進會在 Mac 更新後到達。" + "value": "將 Mac 版 QuotaKit 更新到 0.32.4(fork build 79.1 或更高)。iPhone 1.11.0 仍向前相容舊版 Mac —— 這些改進會在 Mac 更新後到達。" } } } @@ -16529,31 +16529,31 @@ } } }, - "Widgets — QuotaKit Pro adds Home Screen and Lock Screen widgets backed only by sanitized iPhone-side snapshot data, reload immediately after Pro unlock, migrate safely from prior CodexBar local caches, and localize across all four supported languages.": { + "Widgets — QuotaKit Pro adds Home Screen and Lock Screen widgets backed only by sanitized iPhone-side snapshot data, reload immediately after Pro unlock, migrate safely from prior QuotaKit local caches, and localize across all four supported languages.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Widgets — QuotaKit Pro adds Home Screen and Lock Screen widgets backed only by sanitized iPhone-side snapshot data, reload immediately after Pro unlock, migrate safely from prior CodexBar local caches, and localize across all four supported languages." + "value": "Widgets — QuotaKit Pro adds Home Screen and Lock Screen widgets backed only by sanitized iPhone-side snapshot data, reload immediately after Pro unlock, migrate safely from prior QuotaKit local caches, and localize across all four supported languages." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ウィジェット — QuotaKit Pro のホーム画面/ロック画面ウィジェットは整理済み iPhone 側スナップショットのみを使用し、Pro 解除後すぐに更新され、旧 CodexBar ローカルキャッシュから安全に移行し、4 言語すべてにローカライズされます。" + "value": "ウィジェット — QuotaKit Pro のホーム画面/ロック画面ウィジェットは整理済み iPhone 側スナップショットのみを使用し、Pro 解除後すぐに更新され、旧 QuotaKit ローカルキャッシュから安全に移行し、4 言語すべてにローカライズされます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "小组件 —— QuotaKit Pro 的主屏幕和锁屏小组件只读取经过清理的 iPhone 端快照,Pro 解锁后会立即刷新,并可从旧 CodexBar 本地缓存安全迁移,且支持全部四种语言。" + "value": "小组件 —— QuotaKit Pro 的主屏幕和锁屏小组件只读取经过清理的 iPhone 端快照,Pro 解锁后会立即刷新,并可从旧 QuotaKit 本地缓存安全迁移,且支持全部四种语言。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "小工具 —— QuotaKit Pro 的主畫面與鎖定畫面小工具只讀取經過清理的 iPhone 端快照,Pro 解鎖後會立即重新整理,並可從舊 CodexBar 本地快取安全移轉,且支援全部四種語言。" + "value": "小工具 —— QuotaKit Pro 的主畫面與鎖定畫面小工具只讀取經過清理的 iPhone 端快照,Pro 解鎖後會立即重新整理,並可從舊 QuotaKit 本地快取安全移轉,且支援全部四種語言。" } } } @@ -17829,31 +17829,31 @@ } } }, - "Sync refresh feedback — pull-to-refresh and the synced-time chip now show a visible refreshing state until iCloud finishes, and the last-synced age keeps counting from the Mac-confirmed sync time.": { + "Sync polish — provider colors now stay distinct and readable in both appearances, and the synced-time chip keeps its status available to VoiceOver while refreshing.": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sync refresh feedback — pull-to-refresh and the synced-time chip now show a visible refreshing state until iCloud finishes, and the last-synced age keeps counting from the Mac-confirmed sync time." + "value": "Sync polish — provider colors now stay distinct and readable in both appearances, and the synced-time chip keeps its status available to VoiceOver while refreshing." } }, "ja": { "stringUnit": { "state": "translated", - "value": "同期更新のフィードバック — 引っ張って更新したときや同期時刻チップをタップしたとき、iCloud が完了するまで更新中の状態を表示し、最終同期の経過時間は Mac で確認された同期時刻から数え続けます。" + "value": "同期の磨き込み — provider 色は両方の表示モードで見分けやすく読みやすいままになり、同期時刻チップは更新中も VoiceOver で状態を確認できます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "同步刷新反馈 —— 下拉刷新和同步时间标签现在会在 iCloud 完成前持续显示刷新状态,且上次同步时间会从 Mac 确认的同步时间开始持续计时。" + "value": "同步细节优化 —— provider 颜色现在在两种外观下都保持清晰可辨,同步时间标签在刷新时也会继续向 VoiceOver 提供状态。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "同步更新回饋 —— 下拉更新和同步時間標籤現在會在 iCloud 完成前持續顯示更新狀態,且上次同步時間會從 Mac 確認的同步時間開始持續計時。" + "value": "同步細節優化 —— provider 顏色現在在兩種外觀下都保持清晰可辨,同步時間標籤在更新時也會繼續向 VoiceOver 提供狀態。" } } } diff --git a/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift b/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift index 29c40298..5bc429ff 100644 --- a/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift +++ b/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift @@ -2,215 +2,123 @@ import SwiftUI /// Single source of truth for provider-card tint colors. /// -/// Before iOS 1.3.0 / Build 70 this logic was duplicated (with subtle -/// drift) across 5 call sites: `ProviderUsageView.providerColor`, -/// `ProviderDetailView.providerColor`, `UtilizationAggregateView.providerColor(for:)`, -/// `ContentView.providerTint(for:)`, and `CostShareService.providerColor(for:)`. -/// Any new provider (e.g. Perplexity / OpenCode Go from upstream 0.20) had to -/// be added in 5 places or face color collisions across tabs. -/// -/// Pass the `providerID` (the lowercase canonical ID like `"perplexity"` or -/// `"opencodego"`) — not the display name. The function lowercases + strips -/// spaces defensively so passing a display name still works, but prefer ID. +/// The raw swatches mirror the Mac `ProviderDescriptorRegistry` branding +/// colors. `color(for:)` returns an appearance-adaptive tint so very dark or +/// very light brand colors stay visible on iOS surfaces. enum ProviderColorPalette { - /// Returns the brand-aligned tint color for a provider. - /// - /// New provider additions MUST check the specificity ordering — narrower - /// matches (`opencodego`) go **before** broader substrings (`opencode`) - /// so we don't accidentally collapse two distinct providers back into the - /// same color. - static func color(for providerIdentifier: String) -> Color { - let normalized = providerIdentifier - .lowercased() - .replacingOccurrences(of: " ", with: "") + struct RawColor: Equatable { + let red: Double + let green: Double + let blue: Double - // Mirrors the Mac ProviderDescriptorRegistry branding colors for - // customer-facing provider UI. Keep narrow matches before broad ones. - // - // Specific new providers from upstream v0.20 — these come first - // because `opencodego.contains("opencode")` would otherwise grab the - // more general rule below and collapse Go into Zen's blue. - if normalized.contains("perplexity") { - return Color(red: 32 / 255, green: 178 / 255, blue: 170 / 255) - } - if normalized.contains("opencodego") { - return Color(red: 59 / 255, green: 130 / 255, blue: 246 / 255) + var color: Color { + Color(uiColor: UIColor { traits in + let adapted = self.adapted(forDarkMode: traits.userInterfaceStyle == .dark) + return UIColor( + red: adapted.red, + green: adapted.green, + blue: adapted.blue, + alpha: 1) + }) } - // Specific new providers from upstream v0.21 / v0.23 (iOS 1.5.0). - if normalized.contains("abacus") { - return Color(red: 56 / 255, green: 189 / 255, blue: 248 / 255) - } - if normalized.contains("mistral") { - return Color(red: 255 / 255, green: 80 / 255, blue: 15 / 255) + private var luminance: Double { + 0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue } - // Specific new providers from upstream v0.24 / v0.25 (iOS 1.6.0). - // 10 picks; the 11th catch-up provider (`openai`, OpenAI API balance - // from v0.25) inherits the existing ChatGPT-green rule below since - // both share the `openai` providerID. - // - if normalized.contains("windsurf") { - return Color(red: 52 / 255, green: 232 / 255, blue: 187 / 255) - } - if normalized.contains("codebuff") { - return Color(red: 68 / 255, green: 255 / 255, blue: 0 / 255) - } - if normalized.contains("deepseek") { - return Color(red: 0.32, green: 0.49, blue: 0.94) - } - if normalized.contains("manus") { - return Color(red: 52 / 255, green: 50 / 255, blue: 45 / 255) - } - if normalized.contains("mimo") { - return Color(red: 1.0, green: 105 / 255, blue: 0) - } - if normalized.contains("doubao") { - return Color(red: 51 / 255, green: 112 / 255, blue: 255 / 255) - } - if normalized.contains("commandcode") { - return Color(red: 0, green: 0, blue: 0) - } - if normalized.contains("stepfun") { - return Color(red: 0.13, green: 0.59, blue: 0.95) - } - if normalized.contains("crof") { - return Color(red: 0.18, green: 0.67, blue: 0.58) - } - if normalized.contains("venice") { - return Color(red: 0.2, green: 0.6, blue: 1.0) + private func adapted(forDarkMode isDarkMode: Bool) -> RawColor { + if isDarkMode, self.luminance < 0.22 { + return self.mixed(with: RawColor(red: 1, green: 1, blue: 1), amount: 0.48) + } + if !isDarkMode, self.luminance > 0.82 { + return self.mixed(with: RawColor(red: 0, green: 0, blue: 0), amount: 0.42) + } + return self } - // iOS 1.8.0 — upstream v0.27.0 new providers (5 picks). - // Color choices avoid the existing palette zones; new entries - // sit beside their conceptual cluster (Grok/Groq both "warm" - // brand-aligned shades distinct from Mistral red, ElevenLabs - // pure-voice teal, Deepgram brand purple, LLM Proxy neutral - // slate since it's a meta-provider). - if normalized.contains("grok") { - return Color(red: 16 / 255, green: 163 / 255, blue: 127 / 255) - } - if normalized.contains("groq") { - return Color(red: 245 / 255, green: 104 / 255, blue: 68 / 255) - } - if normalized.contains("elevenlabs") { - return Color(red: 0.92, green: 0.92, blue: 0.90) - } - if normalized.contains("deepgram") { - // Deepgram — brand purple (#7C3AED). Distinct from - // codex/cursor `.purple` (~#800080) by being more - // saturated and bluer; sits between codex and openrouter - // in the purple cluster without collapsing into either. - return Color(red: 0.49, green: 0.23, blue: 0.93) - } - if normalized.contains("llmproxy") || normalized.contains("llm-proxy") { - return Color(red: 36 / 255, green: 180 / 255, blue: 126 / 255) + private func mixed(with other: RawColor, amount: Double) -> RawColor { + RawColor( + red: self.red + (other.red - self.red) * amount, + green: self.green + (other.green - self.green) * amount, + blue: self.blue + (other.blue - self.blue) * amount) } + } - // iOS 1.9.0 — upstream v0.28.0+v0.29.0 new providers (3 picks). - // Checked BEFORE the generic `openai`/`opencode` rules below: - // `"azureopenai".contains("openai")` is true, so Azure OpenAI must - // match here first or it would collapse into the ChatGPT-green rule. - if normalized.contains("azureopenai") { - // Azure OpenAI — Microsoft Azure blue (#0078D4). Distinct from - // the opencode `.blue` fallback and deepseek royal blue by being - // a cleaner mid cyan-blue tied to the Azure brand. - return Color(red: 0.0, green: 0.47, blue: 0.83) - } - if normalized.contains("alibabatokenplan") { - return Color(red: 1.0, green: 106 / 255, blue: 0) - } - if normalized.contains("alibaba") { - return Color(red: 1.0, green: 106 / 255, blue: 0) - } - if normalized.contains("t3chat") { - return Color(red: 245 / 255, green: 102 / 255, blue: 71 / 255) - } + static func color(for providerIdentifier: String) -> Color { + (self.rawColor(for: providerIdentifier) ?? Self.fallback).color + } - // iOS 1.7.0 — upstream v0.26.0 new providers. - if normalized.contains("moonshot") || normalized.contains("kimi-api") { - return Color(red: 32 / 255, green: 93 / 255, blue: 235 / 255) - } - if normalized.contains("bedrock") { - // AWS Bedrock — AWS-orange (#FF9900). The most recognizable - // AWS brand tint; reads cleanly against the cost-budget - // gradient on the dedicated card. - return Color(red: 1.00, green: 0.60, blue: 0.00) - } - // Earlier upstream providers without explicit entries (falls - // back to .blue otherwise). Adding distinct tints so the - // multi-card grid stays legible. - if normalized.contains("kiro") { - return Color(red: 255 / 255, green: 153 / 255, blue: 0 / 255) - } - if normalized.contains("zai") || normalized.contains("z.ai") { - return Color(red: 232 / 255, green: 90 / 255, blue: 106 / 255) - } - if normalized.contains("antigravity") { - return Color(red: 96 / 255, green: 186 / 255, blue: 126 / 255) - } - if normalized.contains("factory") || normalized.contains("droid") { - return Color(red: 255 / 255, green: 107 / 255, blue: 53 / 255) - } - if normalized.contains("copilot") { - return Color(red: 168 / 255, green: 85 / 255, blue: 247 / 255) - } - if normalized.contains("kimik2") || normalized.contains("kimik2unofficial") { - return Color(red: 76 / 255, green: 0 / 255, blue: 255 / 255) - } - if normalized.contains("kimi") { - return Color(red: 254 / 255, green: 96 / 255, blue: 60 / 255) - } - if normalized.contains("minimax") { - return Color(red: 254 / 255, green: 96 / 255, blue: 60 / 255) - } - if normalized.contains("kilo") { - return Color(red: 242 / 255, green: 112 / 255, blue: 39 / 255) - } - if normalized.contains("vertexai") || normalized.contains("vertex") { - return Color(red: 66 / 255, green: 133 / 255, blue: 244 / 255) - } - if normalized.contains("augment") { - return Color(red: 99 / 255, green: 102 / 255, blue: 241 / 255) - } - if normalized.contains("jetbrains") { - return Color(red: 255 / 255, green: 51 / 255, blue: 153 / 255) - } - if normalized.contains("amp") { - return Color(red: 220 / 255, green: 38 / 255, blue: 38 / 255) - } - if normalized.contains("ollama") { - return Color(red: 136 / 255, green: 136 / 255, blue: 136 / 255) - } - if normalized.contains("synthetic") { - return Color(red: 20 / 255, green: 20 / 255, blue: 20 / 255) - } - if normalized.contains("warp") { - return Color(red: 147 / 255, green: 139 / 255, blue: 180 / 255) - } + static func rawColor(for providerIdentifier: String) -> RawColor? { + Self.palette[Self.normalized(providerIdentifier)] + } - // Existing provider mappings — preserved from pre-1.3.0 behavior. - if normalized.contains("claude") || normalized.contains("anthropic") { - return Color(red: 204 / 255, green: 124 / 255, blue: 94 / 255) - } - if normalized.contains("codex") { - return Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255) - } - if normalized.contains("cursor") { - return Color(red: 0, green: 0, blue: 0) - } - if normalized.contains("openai") || normalized.contains("chatgpt") { - return Color(red: 0.06, green: 0.51, blue: 0.43) - } - if normalized.contains("gemini") { - return Color(red: 171 / 255, green: 135 / 255, blue: 234 / 255) - } - if normalized.contains("openrouter") { - return Color(red: 100 / 255, green: 103 / 255, blue: 242 / 255) - } - if normalized.contains("opencode") { - return Color(red: 59 / 255, green: 130 / 255, blue: 246 / 255) - } - return .blue + static func normalized(_ value: String) -> String { + String(value + .lowercased() + .unicodeScalars + .filter { CharacterSet.alphanumerics.contains($0) }) } + + private static let fallback = RawColor(red: 0, green: 122 / 255, blue: 1) + + private static let palette: [String: RawColor] = { + let entries: [(aliases: [String], color: RawColor)] = [ + (["codex"], RawColor(red: 73 / 255, green: 163 / 255, blue: 176 / 255)), + (["openai", "chatgpt"], RawColor(red: 0.06, green: 0.51, blue: 0.43)), + (["azureopenai"], RawColor(red: 0, green: 120 / 255, blue: 212 / 255)), + (["claude", "anthropic"], RawColor(red: 204 / 255, green: 124 / 255, blue: 94 / 255)), + (["cursor"], RawColor(red: 0, green: 0, blue: 0)), + (["opencode"], RawColor(red: 14 / 255, green: 165 / 255, blue: 233 / 255)), + (["opencodego"], RawColor(red: 52 / 255, green: 211 / 255, blue: 153 / 255)), + (["alibaba", "bailian"], RawColor(red: 1, green: 106 / 255, blue: 0)), + (["alibabatokenplan", "alibabatoken", "bailiantokenplan"], RawColor(red: 1, green: 176 / 255, blue: 32 / 255)), + (["factory", "droid"], RawColor(red: 255 / 255, green: 107 / 255, blue: 53 / 255)), + (["gemini"], RawColor(red: 171 / 255, green: 135 / 255, blue: 234 / 255)), + (["antigravity"], RawColor(red: 96 / 255, green: 186 / 255, blue: 126 / 255)), + (["copilot"], RawColor(red: 168 / 255, green: 85 / 255, blue: 247 / 255)), + (["zai"], RawColor(red: 232 / 255, green: 90 / 255, blue: 106 / 255)), + (["minimax"], RawColor(red: 239 / 255, green: 68 / 255, blue: 68 / 255)), + (["manus"], RawColor(red: 63 / 255, green: 58 / 255, blue: 50 / 255)), + (["kimi"], RawColor(red: 244 / 255, green: 63 / 255, blue: 94 / 255)), + (["kilo"], RawColor(red: 242 / 255, green: 112 / 255, blue: 39 / 255)), + (["kiro"], RawColor(red: 217 / 255, green: 119 / 255, blue: 6 / 255)), + (["vertexai", "vertex"], RawColor(red: 66 / 255, green: 133 / 255, blue: 244 / 255)), + (["augment"], RawColor(red: 139 / 255, green: 92 / 255, blue: 246 / 255)), + (["jetbrains"], RawColor(red: 255 / 255, green: 51 / 255, blue: 153 / 255)), + (["kimik2", "kimik2unofficial"], RawColor(red: 76 / 255, green: 0, blue: 255 / 255)), + (["moonshot", "moonshotkimiapi", "kimiapi"], RawColor(red: 32 / 255, green: 93 / 255, blue: 235 / 255)), + (["amp", "ampcode"], RawColor(red: 220 / 255, green: 38 / 255, blue: 38 / 255)), + (["t3chat", "t3"], RawColor(red: 219 / 255, green: 39 / 255, blue: 119 / 255)), + (["ollama"], RawColor(red: 136 / 255, green: 136 / 255, blue: 136 / 255)), + (["synthetic", "syntheticnew"], RawColor(red: 42 / 255, green: 42 / 255, blue: 42 / 255)), + (["warp"], RawColor(red: 147 / 255, green: 139 / 255, blue: 180 / 255)), + (["openrouter"], RawColor(red: 100 / 255, green: 103 / 255, blue: 242 / 255)), + (["elevenlabs", "11labs", "eleven"], RawColor(red: 0.92, green: 0.92, blue: 0.90)), + (["windsurf"], RawColor(red: 52 / 255, green: 232 / 255, blue: 187 / 255)), + (["perplexity"], RawColor(red: 32 / 255, green: 178 / 255, blue: 170 / 255)), + (["mimo", "xiaomimimo"], RawColor(red: 249 / 255, green: 115 / 255, blue: 22 / 255)), + (["doubao"], RawColor(red: 51 / 255, green: 112 / 255, blue: 255 / 255)), + (["abacus", "abacusai"], RawColor(red: 56 / 255, green: 189 / 255, blue: 248 / 255)), + (["mistral"], RawColor(red: 255 / 255, green: 80 / 255, blue: 15 / 255)), + (["deepseek"], RawColor(red: 0.32, green: 0.49, blue: 0.94)), + (["codebuff"], RawColor(red: 68 / 255, green: 255 / 255, blue: 0)), + (["crof"], RawColor(red: 0.18, green: 0.67, blue: 0.58)), + (["venice"], RawColor(red: 0.2, green: 0.6, blue: 1)), + (["commandcode"], RawColor(red: 71 / 255, green: 85 / 255, blue: 105 / 255)), + (["stepfun"], RawColor(red: 0.13, green: 0.59, blue: 0.95)), + (["bedrock"], RawColor(red: 1, green: 0.6, blue: 0)), + (["grok"], RawColor(red: 26 / 255, green: 26 / 255, blue: 26 / 255)), + (["groq", "groqcloud", "groqapi"], RawColor(red: 245 / 255, green: 104 / 255, blue: 68 / 255)), + (["llmproxy"], RawColor(red: 36 / 255, green: 180 / 255, blue: 126 / 255)), + (["deepgram"], RawColor(red: 0.49, green: 0.23, blue: 0.93)), + ] + + var table: [String: RawColor] = [:] + for entry in entries { + for alias in entry.aliases { + table[Self.normalized(alias)] = entry.color + } + } + return table + }() } diff --git a/CodexBarMobile/CodexBarMobileTests/MultiAccountTabRenderingTests.swift b/CodexBarMobile/CodexBarMobileTests/MultiAccountTabRenderingTests.swift index b356f34b..bdd8184c 100644 --- a/CodexBarMobile/CodexBarMobileTests/MultiAccountTabRenderingTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/MultiAccountTabRenderingTests.swift @@ -19,6 +19,15 @@ import XCTest /// new `group: ProviderAccountGroup` parameter. @MainActor final class MultiAccountTabRenderingTests: XCTestCase { + nonisolated private static let remoteConfigSuiteName = "com.columbuslabs.quotakit.tests.multi-account" + + override func setUp() { + super.setUp() + UserDefaults.standard.removePersistentDomain(forName: Self.remoteConfigSuiteName) + UserDefaults(suiteName: Self.remoteConfigSuiteName)? + .removePersistentDomain(forName: Self.remoteConfigSuiteName) + } + private func snapshot( providerID: String, providerName: String, @@ -40,6 +49,7 @@ final class MultiAccountTabRenderingTests: XCTestCase { private func renderToImage(_ view: V) -> UIImage? { let renderer = ImageRenderer(content: view .environment(ProEntitlementStore.preview(state: .unlocked(source: .storeKit))) + .environment(RemoteConfigStore(defaults: UserDefaults(suiteName: Self.remoteConfigSuiteName))) .frame(width: 390, height: 800)) renderer.scale = 2.0 return renderer.uiImage diff --git a/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift b/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift index 3bc1354d..07698a83 100644 --- a/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift @@ -1,11 +1,10 @@ -import SwiftUI import Testing @testable import CodexBarMobile @Suite("Provider color palette") struct ProviderColorPaletteTests { - @Test("Priority providers use QuotaKit-approved brand colors") + @Test("Priority providers use QuotaKit-approved raw colors") func priorityProviderColors() { expectColor("codex", red: 73 / 255, green: 163 / 255, blue: 176 / 255) expectColor("claude", red: 204 / 255, green: 124 / 255, blue: 94 / 255) @@ -13,60 +12,51 @@ struct ProviderColorPaletteTests { expectColor("cursor", red: 0, green: 0, blue: 0) } - @Test("Codex and Cursor no longer collide") - func codexAndCursorAreDistinct() { - let codex = UIColor(ProviderColorPalette.color(for: "codex")) - let cursor = UIColor(ProviderColorPalette.color(for: "cursor")) - #expect(!codex.isApproximately(cursor)) - #expect(!codex.isApproximately(UIColor(.purple))) - #expect(!cursor.isApproximately(UIColor(.purple))) - } - @Test("Palette mirrors Mac descriptor colors for known providers") func knownProviderColors() { - let expected: [(String, CGFloat, CGFloat, CGFloat)] = [ + let expected: [(String, Double, Double, Double)] = [ ("openai", 0.06, 0.51, 0.43), ("azureopenai", 0, 120 / 255, 212 / 255), - ("opencode", 59 / 255, 130 / 255, 246 / 255), - ("opencodego", 59 / 255, 130 / 255, 246 / 255), - ("alibaba", 1.0, 106 / 255, 0), - ("alibabatokenplan", 1.0, 106 / 255, 0), + ("opencode", 14 / 255, 165 / 255, 233 / 255), + ("opencodego", 52 / 255, 211 / 255, 153 / 255), + ("alibaba", 1, 106 / 255, 0), + ("alibabatokenplan", 1, 176 / 255, 32 / 255), ("factory", 255 / 255, 107 / 255, 53 / 255), ("gemini", 171 / 255, 135 / 255, 234 / 255), ("antigravity", 96 / 255, 186 / 255, 126 / 255), ("copilot", 168 / 255, 85 / 255, 247 / 255), ("zai", 232 / 255, 90 / 255, 106 / 255), - ("minimax", 254 / 255, 96 / 255, 60 / 255), - ("manus", 52 / 255, 50 / 255, 45 / 255), - ("kimi", 254 / 255, 96 / 255, 60 / 255), + ("minimax", 239 / 255, 68 / 255, 68 / 255), + ("manus", 63 / 255, 58 / 255, 50 / 255), + ("kimi", 244 / 255, 63 / 255, 94 / 255), ("kimik2", 76 / 255, 0, 255 / 255), ("kilo", 242 / 255, 112 / 255, 39 / 255), - ("kiro", 255 / 255, 153 / 255, 0), + ("kiro", 217 / 255, 119 / 255, 6 / 255), ("vertexai", 66 / 255, 133 / 255, 244 / 255), - ("augment", 99 / 255, 102 / 255, 241 / 255), + ("augment", 139 / 255, 92 / 255, 246 / 255), ("jetbrains", 255 / 255, 51 / 255, 153 / 255), ("moonshot", 32 / 255, 93 / 255, 235 / 255), ("amp", 220 / 255, 38 / 255, 38 / 255), - ("t3chat", 245 / 255, 102 / 255, 71 / 255), + ("t3chat", 219 / 255, 39 / 255, 119 / 255), ("ollama", 136 / 255, 136 / 255, 136 / 255), - ("synthetic", 20 / 255, 20 / 255, 20 / 255), + ("synthetic", 42 / 255, 42 / 255, 42 / 255), ("warp", 147 / 255, 139 / 255, 180 / 255), ("openrouter", 100 / 255, 103 / 255, 242 / 255), ("elevenlabs", 0.92, 0.92, 0.90), ("windsurf", 52 / 255, 232 / 255, 187 / 255), ("perplexity", 32 / 255, 178 / 255, 170 / 255), - ("mimo", 1.0, 105 / 255, 0), + ("mimo", 249 / 255, 115 / 255, 22 / 255), ("doubao", 51 / 255, 112 / 255, 255 / 255), ("abacus", 56 / 255, 189 / 255, 248 / 255), ("mistral", 255 / 255, 80 / 255, 15 / 255), ("deepseek", 0.32, 0.49, 0.94), ("codebuff", 68 / 255, 255 / 255, 0), ("crof", 0.18, 0.67, 0.58), - ("venice", 0.2, 0.6, 1.0), - ("commandcode", 0, 0, 0), + ("venice", 0.2, 0.6, 1), + ("commandcode", 71 / 255, 85 / 255, 105 / 255), ("stepfun", 0.13, 0.59, 0.95), - ("bedrock", 1.0, 0.6, 0), - ("grok", 16 / 255, 163 / 255, 127 / 255), + ("bedrock", 1, 0.6, 0), + ("grok", 26 / 255, 26 / 255, 26 / 255), ("groq", 245 / 255, 104 / 255, 68 / 255), ("llmproxy", 36 / 255, 180 / 255, 126 / 255), ("deepgram", 0.49, 0.23, 0.93), @@ -86,47 +76,66 @@ struct ProviderColorPaletteTests { ("Moonshot / Kimi API", "moonshot"), ("Azure OpenAI", "azureopenai"), ("Alibaba Token Plan", "alibabatokenplan"), + ("Xiaomi MiMo", "mimo"), + ("GroqCloud", "groq"), ] for (displayName, providerID) in pairs { - let byName = UIColor(ProviderColorPalette.color(for: displayName)) - let byID = UIColor(ProviderColorPalette.color(for: providerID)) - #expect(byName.isApproximately(byID), "\(displayName) should match \(providerID)") + #expect( + ProviderColorPalette.rawColor(for: displayName) == ProviderColorPalette.rawColor(for: providerID), + "\(displayName) should match \(providerID)") } } - @Test("Unknown and empty provider IDs still fall back to blue") - func unknownFallsToBlue() { - #expect(UIColor(ProviderColorPalette.color(for: "")).isApproximately(UIColor(.blue))) - #expect(UIColor(ProviderColorPalette.color(for: "brand-new-ai-tool")).isApproximately(UIColor(.blue))) + @Test("Substring matches do not steal unrelated provider names") + func substringMatchesDoNotStealUnrelatedNames() { + #expect(ProviderColorPalette.rawColor(for: "example-provider") == nil) + #expect(ProviderColorPalette.rawColor(for: "lamp") == nil) + #expect(ProviderColorPalette.rawColor(for: "opencodegoose") == nil) + #expect(ProviderColorPalette.rawColor(for: "chatgpt") == ProviderColorPalette.rawColor(for: "openai")) } -} -private func expectColor(_ provider: String, red: CGFloat, green: CGFloat, blue: CGFloat) { - let color = UIColor(ProviderColorPalette.color(for: provider)) - let expected = UIColor(red: red, green: green, blue: blue, alpha: 1) - #expect(color.isApproximately(expected), "\(provider) color did not match expected swatch") -} + @Test("Known provider colors stay visually distinct") + func knownProviderColorsStayDistinct() { + let providers = [ + "codex", "openai", "azureopenai", "claude", "cursor", "opencode", "opencodego", + "alibaba", "alibabatokenplan", "factory", "gemini", "antigravity", "copilot", + "zai", "minimax", "manus", "kimi", "kilo", "kiro", "vertexai", "augment", + "jetbrains", "kimik2", "moonshot", "amp", "t3chat", "ollama", "synthetic", + "warp", "openrouter", "elevenlabs", "windsurf", "perplexity", "mimo", + "doubao", "abacus", "mistral", "deepseek", "codebuff", "crof", "venice", + "commandcode", "stepfun", "bedrock", "grok", "groq", "llmproxy", "deepgram", + ] + let allowedSharedBrandPairs: Set> = [] -extension UIColor { - fileprivate func isApproximately(_ other: UIColor, tolerance: CGFloat = 0.02) -> Bool { - var lhsR: CGFloat = 0 - var lhsG: CGFloat = 0 - var lhsB: CGFloat = 0 - var lhsA: CGFloat = 0 - var rhsR: CGFloat = 0 - var rhsG: CGFloat = 0 - var rhsB: CGFloat = 0 - var rhsA: CGFloat = 0 - guard - getRed(&lhsR, green: &lhsG, blue: &lhsB, alpha: &lhsA), - other.getRed(&rhsR, green: &rhsG, blue: &rhsB, alpha: &rhsA) - else { - return false + for leftIndex in providers.indices { + for rightIndex in providers.index(after: leftIndex).. 0.10, "\(left) and \(right) must stay visually distinct (delta: \(delta))") + } } - return abs(lhsR - rhsR) < tolerance - && abs(lhsG - rhsG) < tolerance - && abs(lhsB - rhsB) < tolerance - && abs(lhsA - rhsA) < tolerance } + + @Test("Unknown and empty provider IDs still fall back at render time") + func unknownFallsBackAtRenderTime() { + #expect(ProviderColorPalette.rawColor(for: "") == nil) + #expect(ProviderColorPalette.rawColor(for: "brand-new-ai-tool") == nil) + } +} + +private func expectColor(_ provider: String, red: Double, green: Double, blue: Double) { + let color = ProviderColorPalette.rawColor(for: provider) + #expect(color != nil, "\(provider) should have a raw palette entry") + #expect(abs((color?.red ?? -1) - red) < 0.001, "\(provider) red channel did not match") + #expect(abs((color?.green ?? -1) - green) < 0.001, "\(provider) green channel did not match") + #expect(abs((color?.blue ?? -1) - blue) < 0.001, "\(provider) blue channel did not match") } diff --git a/CodexBarMobile/CodexBarMobileTests/QuotaKitProViewSmokeTests.swift b/CodexBarMobile/CodexBarMobileTests/QuotaKitProViewSmokeTests.swift index a2b44a4a..ceedb45d 100644 --- a/CodexBarMobile/CodexBarMobileTests/QuotaKitProViewSmokeTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/QuotaKitProViewSmokeTests.swift @@ -6,10 +6,14 @@ import XCTest @MainActor final class QuotaKitProViewSmokeTests: XCTestCase { + nonisolated private static let remoteConfigSuiteName = "com.columbuslabs.quotakit.tests.pro-smoke" override func setUp() { super.setUp() UserDefaults.standard.removeObject(forKey: MobileSettingsKeys.freeSelectedProviderID) + UserDefaults.standard.removePersistentDomain(forName: Self.remoteConfigSuiteName) + UserDefaults(suiteName: Self.remoteConfigSuiteName)? + .removePersistentDomain(forName: Self.remoteConfigSuiteName) } override func tearDown() { @@ -143,7 +147,10 @@ final class QuotaKitProViewSmokeTests: XCTestCase { } private func renderToImage(_ view: V) -> UIImage? { - let renderer = ImageRenderer(content: view.frame(width: 390, height: 900).quotaKitThemed()) + let renderer = ImageRenderer(content: view + .environment(RemoteConfigStore(defaults: UserDefaults(suiteName: Self.remoteConfigSuiteName))) + .frame(width: 390, height: 900) + .quotaKitThemed()) renderer.scale = 2.0 return renderer.uiImage } diff --git a/CodexBarMobile/Research/034-review-findings-fix-bundle.md b/CodexBarMobile/Research/034-review-findings-fix-bundle.md new file mode 100644 index 00000000..8e8872de --- /dev/null +++ b/CodexBarMobile/Research/034-review-findings-fix-bundle.md @@ -0,0 +1,33 @@ +# Review Findings Fix Bundle + +Status: `done` +Date: 2026-06-09 + +## Scope + +This bundle closes the validated review findings from the QuotaKit rebrand and iOS sync freshness pass: + +- Provider tint parity now treats Mac `ProviderDescriptor` colors as canonical, with a script audit to catch mobile drift. +- Known duplicate or near-duplicate colors were separated before parity was enforced. +- The iOS palette now matches exact normalized provider aliases instead of broad substrings, so short names such as `amp` cannot color unrelated future providers. +- Very dark and very light provider swatches adapt at render time for iOS light/dark readability while raw values remain exact for parity tests. +- Sync freshness chips preserve their visible status text for VoiceOver and use a widening timeline cadence as the displayed age gets older. +- The customer branding audit now scans mobile Swift and `.xcstrings` values, and allowlisting is applied to each forbidden-token occurrence instead of an entire line. + +## Decisions + +- Keep raw palette values in sync with Mac descriptors and apply iOS appearance adaptation only in `ProviderColorPalette.color(for:)`. +- Keep `ProviderColorPalette.rawColor(for:)` internal so tests and the parity audit can assert exact RGB values without depending on rendered SwiftUI color behavior. +- Allow explicit upstream MIT attribution wording, but keep generic customer-facing `CodexBar` copy blocked. +- Keep internal target/module/storage names allowlisted only in narrow source contexts. + +## Verification + +- `python3 Scripts/audit_customer_branding.py` +- `python3 Scripts/audit_provider_palette.py` +- `./Scripts/lint.sh audit-customer-branding` +- `./Scripts/lint.sh audit-i18n` +- `./Scripts/lint.sh audit-provider-palette` +- `swift test --filter ProviderRegistryTests` +- `xcodebuild -project CodexBarMobile/CodexBarMobile.xcodeproj -scheme CodexBarMobile -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -skip-testing:CodexBarMobileUITests CODE_SIGNING_ALLOWED=NO test` +- `xcodebuild -project CodexBarMobile/CodexBarMobile.xcodeproj -scheme CodexBarMobile -destination 'platform=iOS Simulator,name=iPhone 17 Pro' CODE_SIGNING_ALLOWED=NO build` diff --git a/CodexBarMobile/Research/README.md b/CodexBarMobile/Research/README.md index 960ecd80..4d77706d 100644 --- a/CodexBarMobile/Research/README.md +++ b/CodexBarMobile/Research/README.md @@ -34,3 +34,4 @@ This directory contains research documents for QuotaKit iOS features and sync wo | 031 | Mac Pace Parity (deficit/reserve pace labels + expected-usage stripe on iOS cards/widgets) | `done` | — | [031-mac-pace-parity.md](031-mac-pace-parity.md) | 2026-06-07 | | 032 | Remote Config OTA Guardrails (public Columbus Labs config for setup URL overrides, feature kill switches, and announcements) | `done` | — | [032-remote-config-ota-guardrails.md](032-remote-config-ota-guardrails.md) | 2026-06-07 | | 033 | iOS Refresh Feedback (tappable sync chip, persistent refresh state, live last-synced age) | `done` | — | [033-ios-refresh-feedback.md](033-ios-refresh-feedback.md) | 2026-06-08 | +| 034 | Review Findings Fix Bundle (provider color parity, adaptive tints, sync accessibility, branding audit hardening) | `done` | — | [034-review-findings-fix-bundle.md](034-review-findings-fix-bundle.md) | 2026-06-09 | diff --git a/Scripts/audit_customer_branding.py b/Scripts/audit_customer_branding.py index 02e9df44..699cb02a 100755 --- a/Scripts/audit_customer_branding.py +++ b/Scripts/audit_customer_branding.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 """Fail on customer-visible legacy CodexBar branding leaks. -Inherited Swift target names, storage identifiers, localization keys, and migration -paths are still allowed. Displayed localization values and raw customer copy are not. +Inherited target names, module imports, storage identifiers, localization keys, +debug log prefixes, and explicit upstream attribution are allowed. Displayed +localization values and raw customer copy must use QuotaKit. """ from __future__ import annotations +import json import os import re import sys @@ -14,15 +16,104 @@ ROOT = Path(__file__).resolve().parents[1] -FORBIDDEN = ("CodexBar", "codexbar", "~/.codexbar") +FORBIDDEN_RE = re.compile(r"CodexBar|codexbar|~/\.codexbar") LOCALIZABLE_RE = re.compile( r'"(?P(?:\\.|[^"\\])*)"\s*=\s*"(?P(?:\\.|[^"\\])*)";' ) +SWIFT_STRING_RE = re.compile(r'"(?:\\.|[^"\\])*"') + +ATTRIBUTION_PATTERNS = [ + re.compile(r"CodexBar by Peter Steinberger"), + re.compile(r"steipete/CodexBar"), + re.compile(r"https://github\.com/steipete/CodexBar"), + re.compile(r"built in part from CodexBar"), +] + +TOKEN_ALLOWLIST_PATTERNS = [ + re.compile(pattern) + for pattern in [ + r"\bimport CodexBar(Core|MacroSupport|Sync)?\b", + r"\bCodexBar[A-Za-z0-9_]+\b", + r"\b[A-Za-z0-9_]+CodexBar[A-Za-z0-9_]*\b", + r"\bCodexbarApp\b", + r"\bCodexBarLog\b", + r"\bCodexBarLogger\b", + r"\bCodexBarConfig\b", + r"\bCodexBarLocalization\b", + r"\bCodexBarMockProvidersEnabled\b", + r"\bCODEXBAR_[A-Z0-9_]+\b", + r"\bMAC_RELEASE_.*CODEXBAR\b", + r"\blegacy[A-Za-z0-9_]*\b", + r"\.codexbar\b", + r"\.codexbar[A-Za-z0-9_]+\b", + r"\bcase codexbar\b", + r"com\.steipete\.codexbar", + r"com\.steipete\.CodexBar", + r"com\.codexbar", + r"CodexBarTeamID", + r"CodexBar_CodexBar", + r"CodexBarClaudeWatchdog", + r"CodexBarLifecycleKeepalive", + r"messageHandlers\?\.codexbarLog", + r"window\.__codexbar", + r"clientName: \"codexbar\"", + r"installationId\": \"codexbar\"", + r"fingerprintId\": \"codexbar-usage\"", + r"__codexbar_", + r"experimental_thread_store=.*codexbar-status", + r"codexbar-[A-Za-z0-9_{}().\\-]+", + r"\"codexbar-\"", + r"\"codexbarLog\"", + r"appendingPathComponent\(\"CodexBar\"", + r"appendingPathComponent\(\"CodexBar\.log\"", + r"appendingPathComponent\(\"codexbar-buy-credits\.log\"", + r"appendingPathComponent\(\"Library/Application Support/CodexBar/ClaudeProbe\"", + r"UserDefaults\(suiteName: \"CodexBar\"\)", + r"Notification\.Name\(\"codexbar[A-Za-z0-9_]+\"\)", + r"\"codexbar\.[A-Za-z0-9_.-]+\"", + r"\"com\.codexbar\.[A-Za-z0-9_.-]+\"", + r"legacyStoreSubdirectoryNames = \[\"CodexBar\"", + r"\[CodexBar [^\]]+\]", + r"CodexBar found another managed account that already uses the current system account\.", + r"CodexBar could not read managed account storage\.", + r"Sources/CodexBar", + r"CodexBarSwiftDataSchema", + ] +] def has_forbidden(text: str) -> bool: - return any(term in text for term in FORBIDDEN) + return FORBIDDEN_RE.search(text) is not None + + +def relative(path: Path) -> str: + return str(path.relative_to(ROOT)) + + +def allowed_by_patterns(text: str, start: int, end: int) -> bool: + for pattern in ATTRIBUTION_PATTERNS + TOKEN_ALLOWLIST_PATTERNS: + for match in pattern.finditer(text): + if match.start() <= start and end <= match.end(): + return True + return False + + +def disallowed_token_count(text: str) -> int: + count = 0 + for match in FORBIDDEN_RE.finditer(text): + if not allowed_by_patterns(text, match.start(), match.end()): + count += 1 + return count + + +def disallowed_swift_literal_on_line(line: str, literal_match: re.Match[str]) -> bool: + """Check only tokens inside a Swift literal, using the full source line for allowlist context.""" + literal_start, literal_end = literal_match.span() + for match in FORBIDDEN_RE.finditer(line, literal_start, literal_end): + if not allowed_by_patterns(line, match.start(), match.end()): + return True + return False def audit_localizable_values() -> list[str]: @@ -34,74 +125,51 @@ def audit_localizable_values() -> list[str]: if not match: continue value = match.group("value") - if has_forbidden(value): - failures.append(f"{path.relative_to(ROOT)}:{line_number}: localized value contains legacy branding") + if disallowed_token_count(value): + failures.append(f"{relative(path)}:{line_number}: localized value contains legacy branding") return failures -SOURCE_ALLOWLIST_PATTERNS = [ - r"\bimport CodexBar(Core|MacroSupport|Sync)?\b", - r"\bCodexBar[A-Za-z0-9_]+\b", - r"\b[A-Za-z0-9_]+CodexBar[A-Za-z0-9_]*\b", - r"\bCodexbarApp\b", - r"\bCodexBarLog\b", - r"\bCodexBarLogger\b", - r"\bCodexBarConfig\b", - r"\bCodexBarLocalization\b", - r"\bCodexBarMockProvidersEnabled\b", - r"\bCODEXBAR_[A-Z0-9_]+\b", - r"\bMAC_RELEASE_.*CODEXBAR\b", - r"\blegacy[A-Za-z0-9_]*\b", - r"\.codexbar\b", - r"\.codexbar[A-Za-z0-9_]+\b", - r"\bcase codexbar\b", - r"com\.steipete\.codexbar", - r"com\.steipete\.CodexBar", - r"com\.codexbar", - r"CodexBarTeamID", - r"CodexBar_CodexBar", - r"CodexBarClaudeWatchdog", - r"CodexBarLifecycleKeepalive", - r"appendingPathComponent\(", - r"UserDefaults\(suiteName:", - r"MigrationItem\(service:", - r"Notification\.Name\(", - r"DispatchQueue\(label:", - r"Logger\(label:", - r"identifier:", - r"logHandlerName", - r"messageHandlers\?\.codexbarLog", - r"window\.__codexbar", - r"clientName: \"codexbar\"", - r"installationId\": \"codexbar\"", - r"fingerprintId\": \"codexbar-usage\"", - r"__codexbar_", - r"experimental_thread_store=.*codexbar-status", - r"codexbar-[A-Za-z0-9_{}().\\-]+", - r"\[CodexBar Sync\]", - r"CodexBar found another managed account that already uses the current system account\.", - r"CodexBar could not read managed account storage\.", -] +def audit_xcstrings_values() -> list[str]: + failures: list[str] = [] + for path in sorted(ROOT.rglob("*.xcstrings")): + if ".build" in path.parts or "DerivedData" in path.parts: + continue + data = json.loads(path.read_text(encoding="utf-8")) + strings = data.get("strings", {}) + for key, entry in strings.items(): + for locale, localization in entry.get("localizations", {}).items(): + value = localization.get("stringUnit", {}).get("value", "") + if disallowed_token_count(value): + failures.append( + f"{relative(path)}:{locale}: localized value for {key[:80]!r} contains legacy branding") + return failures + -SOURCE_ALLOWLIST = [re.compile(pattern) for pattern in SOURCE_ALLOWLIST_PATTERNS] +def source_roots() -> list[Path]: + return [ + ROOT / "Sources" / "CodexBar", + ROOT / "Sources" / "CodexBarCore", + ROOT / "Shared", + ROOT / "CodexBarMobile" / "CodexBarMobile", + ROOT / "CodexBarMobile" / "CodexBarMobilePushExtension", + ROOT / "CodexBarMobile" / "CodexBarMobileWidgets", + ] def allowed_source_line(path: Path, stripped: str) -> bool: if not stripped or stripped.startswith("//") or stripped.startswith("*"): return True if path.name == "KeychainPromptCoordinator.swift" and "L(" not in stripped: - # These are legacy localization keys; displayed Localizable.strings values are audited above. return True if "L(" in stripped and path.parts[-3:] != ("Resources", "en.lproj", "Localizable.strings"): - # Legacy localization lookup keys intentionally retain old English keys. return True - return any(pattern.search(stripped) for pattern in SOURCE_ALLOWLIST) + return False def audit_swift_literals() -> list[str]: failures: list[str] = [] - source_roots = [ROOT / "Sources" / "CodexBar", ROOT / "Sources" / "CodexBarCore", ROOT / "Shared"] - for source_root in source_roots: + for source_root in source_roots(): if not source_root.exists(): continue for path in sorted(source_root.rglob("*.swift")): @@ -113,7 +181,11 @@ def audit_swift_literals() -> list[str]: stripped = line.strip() if allowed_source_line(path, stripped): continue - failures.append(f"{path.relative_to(ROOT)}:{line_number}: possible customer-facing legacy branding") + for string_match in SWIFT_STRING_RE.finditer(line): + if disallowed_swift_literal_on_line(line, string_match): + failures.append( + f"{relative(path)}:{line_number}: possible customer-facing legacy branding") + break return failures @@ -133,13 +205,14 @@ def audit_app_bundle() -> list[str]: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: continue - if has_forbidden(text) and "CodexBar_CodexBar.bundle" not in str(path): + if disallowed_token_count(text) and "CodexBar_CodexBar.bundle" not in str(path): failures.append(f"{path}: packaged file contains legacy branding") return failures def main() -> int: failures = audit_localizable_values() + failures.extend(audit_xcstrings_values()) failures.extend(audit_swift_literals()) failures.extend(audit_app_bundle()) diff --git a/Scripts/audit_provider_palette.py b/Scripts/audit_provider_palette.py new file mode 100755 index 00000000..9236a582 --- /dev/null +++ b/Scripts/audit_provider_palette.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Verify iOS raw provider colors mirror Mac provider descriptors.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MOBILE_PALETTE = ROOT / "CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift" +MAC_PROVIDERS = ROOT / "Sources/CodexBarCore/Providers" + +PROVIDER_RE = re.compile(r"id:\s*\.(?P[A-Za-z0-9_]+)") +COLOR_RE = re.compile( + r"ProviderColor\(\s*red:\s*(?P[^,\)]+),\s*green:\s*(?P[^,\)]+),\s*blue:\s*(?P[^,\)]+)\)" +) +MOBILE_ENTRY_RE = re.compile( + r'\(\["(?P[^"]+)"(?:[^\)]*)RawColor\(red:\s*(?P[^,\)]+),\s*green:\s*(?P[^,\)]+),\s*blue:\s*(?P[^,\)]+)\)\)' +) + + +def evaluate_channel(expression: str) -> float: + parts = [part.strip() for part in expression.split("/")] + if len(parts) == 1: + return float(parts[0]) + if len(parts) == 2: + return float(parts[0]) / float(parts[1]) + raise ValueError(f"Unsupported color expression: {expression}") + + +def parse_color(match: re.Match[str]) -> tuple[float, float, float]: + return ( + evaluate_channel(match.group("red")), + evaluate_channel(match.group("green")), + evaluate_channel(match.group("blue")), + ) + + +def mac_colors() -> dict[str, tuple[float, float, float]]: + colors: dict[str, tuple[float, float, float]] = {} + for path in sorted(MAC_PROVIDERS.rglob("*ProviderDescriptor.swift")): + text = path.read_text(encoding="utf-8") + provider = PROVIDER_RE.search(text) + color = COLOR_RE.search(text) + if provider and color: + colors[provider.group("id")] = parse_color(color) + return colors + + +def mobile_colors() -> dict[str, tuple[float, float, float]]: + text = MOBILE_PALETTE.read_text(encoding="utf-8") + return { + match.group("id"): parse_color(match) + for match in MOBILE_ENTRY_RE.finditer(text) + } + + +def main() -> int: + mac = mac_colors() + mobile = mobile_colors() + failures: list[str] = [] + + for provider, mac_color in sorted(mac.items()): + mobile_color = mobile.get(provider) + if mobile_color is None: + failures.append(f"{provider}: missing from mobile raw palette") + continue + if any(abs(lhs - rhs) > 0.001 for lhs, rhs in zip(mac_color, mobile_color)): + failures.append( + f"{provider}: Mac {mac_color!r} != mobile {mobile_color!r}") + + extra = sorted(set(mobile) - set(mac) - {"chatgpt", "anthropic", "bailian", "vertex"}) + for provider in extra: + failures.append(f"{provider}: mobile canonical alias has no Mac descriptor") + + if failures: + print("ERROR: provider palette parity audit failed:", file=sys.stderr) + for failure in failures: + print(f" {failure}", file=sys.stderr) + return 1 + + print(f"provider palette audit: {len(mac)} Mac descriptors match mobile raw palette") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Scripts/lint.sh b/Scripts/lint.sh index c471d645..c7e29026 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -185,6 +185,10 @@ audit_customer_branding() { python3 "${ROOT_DIR}/Scripts/audit_customer_branding.py" } +audit_provider_palette() { + python3 "${ROOT_DIR}/Scripts/audit_provider_palette.py" +} + cmd="${1:-lint}" case "$cmd" in @@ -194,6 +198,7 @@ case "$cmd" in "${BIN_DIR}/swiftlint" --strict audit_xcstrings audit_customer_branding + audit_provider_palette audit_parser_version check_codex_parser_hash ;; @@ -213,8 +218,11 @@ case "$cmd" in audit-customer-branding) audit_customer_branding ;; + audit-provider-palette) + audit_provider_palette + ;; *) - printf 'Usage: %s [lint|format|audit-i18n|audit-parser-version|audit-parser-hash|audit-customer-branding]\n' "$(basename "$0")" >&2 + printf 'Usage: %s [lint|format|audit-i18n|audit-parser-version|audit-parser-hash|audit-customer-branding|audit-provider-palette]\n' "$(basename "$0")" >&2 exit 2 ;; esac diff --git a/Sources/CodexBar/About.swift b/Sources/CodexBar/About.swift index f6b2e1f4..e4593644 100644 --- a/Sources/CodexBar/About.swift +++ b/Sources/CodexBar/About.swift @@ -27,7 +27,7 @@ func showAbout() { let credits = NSMutableAttributedString( string: "QuotaKit by Columbus Labs\n" + - "Includes MIT-licensed upstream components\n" + + "QuotaKit is built in part from CodexBar by Peter Steinberger, licensed under the MIT License.\n" + "MIT License\n") credits.append(makeLink("GitHub", urlString: "https://github.com/ColumbusLabs/QuotaKit")) credits.append(separator) diff --git a/Sources/CodexBar/PreferencesAboutPane.swift b/Sources/CodexBar/PreferencesAboutPane.swift index 4e893069..d23a5d81 100644 --- a/Sources/CodexBar/PreferencesAboutPane.swift +++ b/Sources/CodexBar/PreferencesAboutPane.swift @@ -113,7 +113,7 @@ struct AboutPane: View { .foregroundStyle(.secondary) } - Text("QuotaKit by Columbus Labs. Includes MIT-licensed upstream components.") + Text("QuotaKit by Columbus Labs. QuotaKit is built in part from CodexBar by Peter Steinberger, licensed under the MIT License.") .font(.footnote) .foregroundStyle(.secondary) .padding(.top, 4) diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanProviderDescriptor.swift index 83f04da4..3b7cd535 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanProviderDescriptor.swift @@ -46,7 +46,7 @@ public enum AlibabaTokenPlanProviderDescriptor { branding: ProviderBranding( iconStyle: .alibaba, iconResourceName: "ProviderIcon-alibaba", - color: ProviderColor(red: 1.0, green: 106 / 255, blue: 0)), + color: ProviderColor(red: 1.0, green: 176 / 255, blue: 32 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Alibaba Token Plan cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift index 7c259d4d..e1c5b89d 100644 --- a/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift @@ -52,7 +52,7 @@ public enum AugmentProviderDescriptor { branding: ProviderBranding( iconStyle: .augment, iconResourceName: "ProviderIcon-augment", - color: ProviderColor(red: 99 / 255, green: 102 / 255, blue: 241 / 255)), + color: ProviderColor(red: 139 / 255, green: 92 / 255, blue: 246 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Augment cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/CommandCode/CommandCodeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/CommandCode/CommandCodeProviderDescriptor.swift index b4ee6735..13d9d079 100644 --- a/Sources/CodexBarCore/Providers/CommandCode/CommandCodeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/CommandCode/CommandCodeProviderDescriptor.swift @@ -29,7 +29,7 @@ public enum CommandCodeProviderDescriptor { branding: ProviderBranding( iconStyle: .commandcode, iconResourceName: "ProviderIcon-commandcode", - color: ProviderColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255)), + color: ProviderColor(red: 71 / 255, green: 85 / 255, blue: 105 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Command Code cost summary is not yet supported." }), diff --git a/Sources/CodexBarCore/Providers/Deepgram/DeepgramProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Deepgram/DeepgramProviderDescriptor.swift index 8490e177..0b8be588 100644 --- a/Sources/CodexBarCore/Providers/Deepgram/DeepgramProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Deepgram/DeepgramProviderDescriptor.swift @@ -29,9 +29,9 @@ public enum DeepgramProviderDescriptor { iconStyle: .deepgram, iconResourceName: "ProviderIcon-deepgram", color: ProviderColor( - red: 100 / 255, - green: 103 / 255, - blue: 242 / 255)), + red: 0.49, + green: 0.23, + blue: 0.93)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { diff --git a/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift index a2d6e8b7..b95829e9 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift @@ -29,7 +29,7 @@ public enum GrokProviderDescriptor { branding: ProviderBranding( iconStyle: .grok, iconResourceName: "ProviderIcon-grok", - color: ProviderColor(red: 16 / 255, green: 163 / 255, blue: 127 / 255)), + color: ProviderColor(red: 26 / 255, green: 26 / 255, blue: 26 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Grok cost summary is not supported yet." }), diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift index 711c20bc..89076566 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum KimiProviderDescriptor { branding: ProviderBranding( iconStyle: .kimi, iconResourceName: "ProviderIcon-kimi", - color: ProviderColor(red: 254 / 255, green: 96 / 255, blue: 60 / 255)), + color: ProviderColor(red: 244 / 255, green: 63 / 255, blue: 94 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Kimi cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift index ad006df0..2edd1258 100644 --- a/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum KiroProviderDescriptor { branding: ProviderBranding( iconStyle: .kiro, iconResourceName: "ProviderIcon-kiro", - color: ProviderColor(red: 255 / 255, green: 153 / 255, blue: 0 / 255)), + color: ProviderColor(red: 217 / 255, green: 119 / 255, blue: 6 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Kiro cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/Manus/ManusProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Manus/ManusProviderDescriptor.swift index 838c6e5e..4534e6c7 100644 --- a/Sources/CodexBarCore/Providers/Manus/ManusProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Manus/ManusProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum ManusProviderDescriptor { branding: ProviderBranding( iconStyle: .manus, iconResourceName: "ProviderIcon-manus", - color: ProviderColor(red: 52 / 255, green: 50 / 255, blue: 45 / 255)), + color: ProviderColor(red: 63 / 255, green: 58 / 255, blue: 50 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Manus cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift index 42e3eb6c..7493282d 100644 --- a/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiMo/MiMoProviderDescriptor.swift @@ -41,7 +41,7 @@ public enum MiMoProviderDescriptor { branding: ProviderBranding( iconStyle: .mimo, iconResourceName: "ProviderIcon-mimo", - color: ProviderColor(red: 1.0, green: 105 / 255, blue: 0)), + color: ProviderColor(red: 249 / 255, green: 115 / 255, blue: 22 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Xiaomi MiMo cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift index 703c9d5e..2a1a9a65 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum MiniMaxProviderDescriptor { branding: ProviderBranding( iconStyle: .minimax, iconResourceName: "ProviderIcon-minimax", - color: ProviderColor(red: 254 / 255, green: 96 / 255, blue: 60 / 255)), + color: ProviderColor(red: 239 / 255, green: 68 / 255, blue: 68 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "MiniMax cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift index eb3d3fb8..8077e981 100644 --- a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum OpenCodeProviderDescriptor { branding: ProviderBranding( iconStyle: .opencode, iconResourceName: "ProviderIcon-opencode", - color: ProviderColor(red: 59 / 255, green: 130 / 255, blue: 246 / 255)), + color: ProviderColor(red: 14 / 255, green: 165 / 255, blue: 233 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "OpenCode cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/OpenCodeGo/OpenCodeGoProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenCodeGo/OpenCodeGoProviderDescriptor.swift index 51b2ac41..17e7b3fc 100644 --- a/Sources/CodexBarCore/Providers/OpenCodeGo/OpenCodeGoProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenCodeGo/OpenCodeGoProviderDescriptor.swift @@ -27,7 +27,7 @@ public enum OpenCodeGoProviderDescriptor { branding: ProviderBranding( iconStyle: .opencodego, iconResourceName: "ProviderIcon-opencodego", - color: ProviderColor(red: 59 / 255, green: 130 / 255, blue: 246 / 255)), + color: ProviderColor(red: 52 / 255, green: 211 / 255, blue: 153 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "OpenCode Go cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift index b0501408..a4a7abf4 100644 --- a/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift @@ -26,7 +26,7 @@ public enum SyntheticProviderDescriptor { branding: ProviderBranding( iconStyle: .synthetic, iconResourceName: "ProviderIcon-synthetic", - color: ProviderColor(red: 20 / 255, green: 20 / 255, blue: 20 / 255)), + color: ProviderColor(red: 42 / 255, green: 42 / 255, blue: 42 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Synthetic cost summary is not supported." }), diff --git a/Sources/CodexBarCore/Providers/T3Chat/T3ChatProviderDescriptor.swift b/Sources/CodexBarCore/Providers/T3Chat/T3ChatProviderDescriptor.swift index 9317b407..313ebfec 100644 --- a/Sources/CodexBarCore/Providers/T3Chat/T3ChatProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/T3Chat/T3ChatProviderDescriptor.swift @@ -28,7 +28,7 @@ public enum T3ChatProviderDescriptor { branding: ProviderBranding( iconStyle: .t3chat, iconResourceName: "ProviderIcon-t3chat", - color: ProviderColor(red: 245 / 255, green: 102 / 255, blue: 71 / 255)), + color: ProviderColor(red: 219 / 255, green: 39 / 255, blue: 119 / 255)), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "T3 Chat cost summary is not supported." }), diff --git a/Tests/CodexBarTests/ProviderRegistryTests.swift b/Tests/CodexBarTests/ProviderRegistryTests.swift index ef8b1db5..aa6a3d42 100644 --- a/Tests/CodexBarTests/ProviderRegistryTests.swift +++ b/Tests/CodexBarTests/ProviderRegistryTests.swift @@ -35,6 +35,25 @@ struct ProviderRegistryTests { expectColor(.codex, red: 73 / 255, green: 163 / 255, blue: 176 / 255) expectColor(.claude, red: 204 / 255, green: 124 / 255, blue: 94 / 255) expectColor(.cursor, red: 0, green: 0, blue: 0) + expectColor(.grok, red: 26 / 255, green: 26 / 255, blue: 26 / 255) + expectColor(.commandcode, red: 71 / 255, green: 85 / 255, blue: 105 / 255) + expectColor(.opencodego, red: 52 / 255, green: 211 / 255, blue: 153 / 255) + } + + @Test + func `provider brand colors stay visually distinct`() { + let descriptors = ProviderDescriptorRegistry.all + + for leftIndex in descriptors.indices { + for rightIndex in descriptors.index(after: leftIndex).. 0.10, "\(left.id.rawValue) and \(right.id.rawValue) colors are too close") + } + } } } From 11d79487568b0dc225f3ac896fd7fcb1b2ac1495 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:12:37 -0400 Subject: [PATCH 3/4] Fix sync empty-state and provider palette adaptation --- .../Models/ProviderColorPalette.swift | 16 +++-- .../Models/SyncedUsageData.swift | 16 ++++- .../ProviderColorPaletteTests.swift | 62 +++++++++++-------- .../CodexBarMobileTests/SyncErrorTests.swift | 53 ++++++++++++++++ Scripts/audit_customer_branding.py | 51 +++++++++++++-- Scripts/audit_provider_palette.py | 57 ++++++++++++++--- Scripts/lint.sh | 1 + 7 files changed, 213 insertions(+), 43 deletions(-) diff --git a/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift b/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift index 5bc429ff..62d258f0 100644 --- a/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift +++ b/CodexBarMobile/CodexBarMobile/Models/ProviderColorPalette.swift @@ -13,7 +13,7 @@ enum ProviderColorPalette { var color: Color { Color(uiColor: UIColor { traits in - let adapted = self.adapted(forDarkMode: traits.userInterfaceStyle == .dark) + let adapted = self.adaptedComponents(forDarkMode: traits.userInterfaceStyle == .dark) return UIColor( red: adapted.red, green: adapted.green, @@ -26,9 +26,17 @@ enum ProviderColorPalette { 0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue } - private func adapted(forDarkMode isDarkMode: Bool) -> RawColor { - if isDarkMode, self.luminance < 0.22 { - return self.mixed(with: RawColor(red: 1, green: 1, blue: 1), amount: 0.48) + func adaptedComponents(forDarkMode isDarkMode: Bool) -> RawColor { + if isDarkMode { + if self.luminance < 0.08 { + return self.mixed(with: RawColor(red: 1, green: 1, blue: 1), amount: 0.40) + } + if self.luminance < 0.14 { + return self.mixed(with: RawColor(red: 1, green: 1, blue: 1), amount: 0.44) + } + if self.luminance < 0.22 { + return self.mixed(with: RawColor(red: 1, green: 1, blue: 1), amount: 0.21) + } } if !isDarkMode, self.luminance > 0.82 { return self.mixed(with: RawColor(red: 0, green: 0, blue: 0), amount: 0.42) diff --git a/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift b/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift index 9ae8c064..0a63cd16 100644 --- a/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift +++ b/CodexBarMobile/CodexBarMobile/Models/SyncedUsageData.swift @@ -329,6 +329,17 @@ final class SyncedUsageData { } } + self.applyFullFetchResults( + perProvider: per, + legacy: legacy, + kvsFallback: reader.latestKVSSnapshot()) + } + + func applyFullFetchResults( + perProvider per: MultiDeviceSyncResult, + legacy: MultiDeviceSyncResult, + kvsFallback: SyncedUsageSnapshot? + ) { // Unpack results per zone. `.error` means transient failure — DO NOT // wipe that bucket, preserve whatever was cached before (Codex // review P1). `.empty` / `.success` are authoritative and DO replace @@ -374,18 +385,19 @@ final class SyncedUsageData { if deviceSnapshots.isEmpty { // Totally empty cloud result. Last-resort KVS fallback. - if let kvsSnapshot = reader.latestKVSSnapshot() { + if let kvsSnapshot = kvsFallback { self.cache.seedFromColdStart([kvsSnapshot]) self.usingKVSFallback = true self.republishFromCache() return } - self.applyRefreshFailureStatus(firstError) if firstError != nil, let snapshot { + self.applyRefreshFailureStatus(firstError) WidgetSnapshotPublisher.publish(from: snapshot) return } self.snapshot = nil + self.syncStatus = firstError.map { .error(message: $0.description) } ?? .noData WidgetSnapshotPublisher.clear() return } diff --git a/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift b/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift index 07698a83..011bc078 100644 --- a/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/ProviderColorPaletteTests.swift @@ -97,32 +97,16 @@ struct ProviderColorPaletteTests { @Test("Known provider colors stay visually distinct") func knownProviderColorsStayDistinct() { - let providers = [ - "codex", "openai", "azureopenai", "claude", "cursor", "opencode", "opencodego", - "alibaba", "alibabatokenplan", "factory", "gemini", "antigravity", "copilot", - "zai", "minimax", "manus", "kimi", "kilo", "kiro", "vertexai", "augment", - "jetbrains", "kimik2", "moonshot", "amp", "t3chat", "ollama", "synthetic", - "warp", "openrouter", "elevenlabs", "windsurf", "perplexity", "mimo", - "doubao", "abacus", "mistral", "deepseek", "codebuff", "crof", "venice", - "commandcode", "stepfun", "bedrock", "grok", "groq", "llmproxy", "deepgram", - ] - let allowedSharedBrandPairs: Set> = [] + expectDistinctColors( + providers: knownDistinctProviders, + color: { ProviderColorPalette.rawColor(for: $0)! }) + } - for leftIndex in providers.indices { - for rightIndex in providers.index(after: leftIndex).. 0.10, "\(left) and \(right) must stay visually distinct (delta: \(delta))") - } - } + @Test("Dark-mode adapted provider colors stay visually distinct") + func darkModeProviderColorsStayDistinct() { + expectDistinctColors( + providers: knownDistinctProviders, + color: { ProviderColorPalette.rawColor(for: $0)!.adaptedComponents(forDarkMode: true) }) } @Test("Unknown and empty provider IDs still fall back at render time") @@ -139,3 +123,31 @@ private func expectColor(_ provider: String, red: Double, green: Double, blue: D #expect(abs((color?.green ?? -1) - green) < 0.001, "\(provider) green channel did not match") #expect(abs((color?.blue ?? -1) - blue) < 0.001, "\(provider) blue channel did not match") } + +private let knownDistinctProviders = [ + "codex", "openai", "azureopenai", "claude", "cursor", "opencode", "opencodego", + "alibaba", "alibabatokenplan", "factory", "gemini", "antigravity", "copilot", + "zai", "minimax", "manus", "kimi", "kilo", "kiro", "vertexai", "augment", + "jetbrains", "kimik2", "moonshot", "amp", "t3chat", "ollama", "synthetic", + "warp", "openrouter", "elevenlabs", "windsurf", "perplexity", "mimo", + "doubao", "abacus", "mistral", "deepseek", "codebuff", "crof", "venice", + "commandcode", "stepfun", "bedrock", "grok", "groq", "llmproxy", "deepgram", +] + +private func expectDistinctColors( + providers: [String], + color: (String) -> ProviderColorPalette.RawColor +) { + for leftIndex in providers.indices { + for rightIndex in providers.index(after: leftIndex).. 0.10, "\(left) and \(right) must stay visually distinct (delta: \(delta))") + } + } +} diff --git a/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift b/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift index a0c87f6e..8133875c 100644 --- a/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift +++ b/CodexBarMobile/CodexBarMobileTests/SyncErrorTests.swift @@ -106,6 +106,59 @@ struct SyncErrorTests { #expect(status.isError == false) } + // MARK: - Full fetch state handling + + @Test("Authoritative empty full fetch clears cached snapshot and shows no data") + @MainActor + func authoritativeEmptyFullFetchClearsCachedSnapshot() { + let data = SyncedUsageData() + data.snapshot = PreviewData.sampleSnapshot + data.syncStatus = .synced(lastConfirmedSync: PreviewData.sampleSnapshot.syncTimestamp) + + data.applyFullFetchResults( + perProvider: .empty, + legacy: .empty, + kvsFallback: nil) + + #expect(data.snapshot == nil) + #expect(data.deviceSnapshots.isEmpty) + #expect(data.syncStatus == .noData) + } + + @Test("Full fetch error preserves cached snapshot and synced status") + @MainActor + func fullFetchErrorPreservesCachedSnapshot() { + let data = SyncedUsageData() + data.snapshot = PreviewData.sampleSnapshot + data.syncStatus = .syncing + + data.applyFullFetchResults( + perProvider: .error(.productionSchemaMissingQueryableIndex("recordName")), + legacy: .error(.networkUnavailable), + kvsFallback: nil) + + #expect(data.snapshot == PreviewData.sampleSnapshot) + #expect(data.syncStatus == .synced(lastConfirmedSync: PreviewData.sampleSnapshot.syncTimestamp)) + } + + @Test("Cold full fetch production index error surfaces as error status") + @MainActor + func coldProductionIndexErrorSurfacesAsError() { + let data = SyncedUsageData() + + data.applyFullFetchResults( + perProvider: .error(.productionSchemaMissingQueryableIndex("recordName")), + legacy: .error(.networkUnavailable), + kvsFallback: nil) + + #expect(data.snapshot == nil) + if case .error(let message) = data.syncStatus { + #expect(message.contains("recordName")) + } else { + Issue.record("Expected .error, got \(data.syncStatus)") + } + } + // MARK: - Sync Freshness Formatting @Test("Sync freshness formatter ticks seconds from confirmed sync") diff --git a/Scripts/audit_customer_branding.py b/Scripts/audit_customer_branding.py index 699cb02a..0c750ef2 100755 --- a/Scripts/audit_customer_branding.py +++ b/Scripts/audit_customer_branding.py @@ -107,6 +107,22 @@ def disallowed_token_count(text: str) -> int: return count +def string_unit_values(node: object) -> list[str]: + values: list[str] = [] + if isinstance(node, dict): + string_unit = node.get("stringUnit") + if isinstance(string_unit, dict): + value = string_unit.get("value") + if isinstance(value, str): + values.append(value) + for child in node.values(): + values.extend(string_unit_values(child)) + elif isinstance(node, list): + for child in node: + values.extend(string_unit_values(child)) + return values + + def disallowed_swift_literal_on_line(line: str, literal_match: re.Match[str]) -> bool: """Check only tokens inside a Swift literal, using the full source line for allowlist context.""" literal_start, literal_end = literal_match.span() @@ -139,10 +155,11 @@ def audit_xcstrings_values() -> list[str]: strings = data.get("strings", {}) for key, entry in strings.items(): for locale, localization in entry.get("localizations", {}).items(): - value = localization.get("stringUnit", {}).get("value", "") - if disallowed_token_count(value): - failures.append( - f"{relative(path)}:{locale}: localized value for {key[:80]!r} contains legacy branding") + for value in string_unit_values(localization): + if disallowed_token_count(value): + failures.append( + f"{relative(path)}:{locale}: localized value for {key[:80]!r} contains legacy branding") + break return failures @@ -211,6 +228,9 @@ def audit_app_bundle() -> list[str]: def main() -> int: + if sys.argv[1:] == ["--self-test"]: + return self_test() + failures = audit_localizable_values() failures.extend(audit_xcstrings_values()) failures.extend(audit_swift_literals()) @@ -229,5 +249,28 @@ def main() -> int: return 0 +def self_test() -> int: + fixture = { + "variations": { + "plural": { + "one": {"stringUnit": {"value": "QuotaKit has one update"}}, + "other": {"stringUnit": {"value": "CodexBar has %lld updates"}}, + } + } + } + values = string_unit_values(fixture) + if values != ["QuotaKit has one update", "CodexBar has %lld updates"]: + print("ERROR: self-test failed to traverse nested string units", file=sys.stderr) + return 1 + if sum(disallowed_token_count(value) for value in values) != 1: + print("ERROR: self-test failed to detect nested legacy branding", file=sys.stderr) + return 1 + if disallowed_token_count("CodexBar") != 1: + print("ERROR: self-test must audit values, not keys", file=sys.stderr) + return 1 + print("customer branding audit self-test: ok") + return 0 + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/Scripts/audit_provider_palette.py b/Scripts/audit_provider_palette.py index 9236a582..844d513d 100755 --- a/Scripts/audit_provider_palette.py +++ b/Scripts/audit_provider_palette.py @@ -17,8 +17,29 @@ r"ProviderColor\(\s*red:\s*(?P[^,\)]+),\s*green:\s*(?P[^,\)]+),\s*blue:\s*(?P[^,\)]+)\)" ) MOBILE_ENTRY_RE = re.compile( - r'\(\["(?P[^"]+)"(?:[^\)]*)RawColor\(red:\s*(?P[^,\)]+),\s*green:\s*(?P[^,\)]+),\s*blue:\s*(?P[^,\)]+)\)\)' + r'\(\[(?P[^\]]+)\],\s*RawColor\(red:\s*(?P[^,\)]+),\s*green:\s*(?P[^,\)]+),\s*blue:\s*(?P[^,\)]+)\)\)' ) +MOBILE_ALIAS_TARGETS = { + "11labs": "elevenlabs", + "abacusai": "abacus", + "ampcode": "amp", + "anthropic": "claude", + "bailian": "alibaba", + "bailiantokenplan": "alibabatokenplan", + "alibabatoken": "alibabatokenplan", + "chatgpt": "openai", + "droid": "factory", + "eleven": "elevenlabs", + "groqapi": "groq", + "groqcloud": "groq", + "kimiapi": "moonshot", + "kimik2unofficial": "kimik2", + "moonshotkimiapi": "moonshot", + "syntheticnew": "synthetic", + "t3": "t3chat", + "vertex": "vertexai", + "xiaomimimo": "mimo", +} def evaluate_channel(expression: str) -> float: @@ -49,17 +70,27 @@ def mac_colors() -> dict[str, tuple[float, float, float]]: return colors -def mobile_colors() -> dict[str, tuple[float, float, float]]: +def mobile_palette() -> tuple[ + dict[str, tuple[float, float, float]], + dict[str, str], +]: text = MOBILE_PALETTE.read_text(encoding="utf-8") - return { - match.group("id"): parse_color(match) - for match in MOBILE_ENTRY_RE.finditer(text) - } + colors: dict[str, tuple[float, float, float]] = {} + aliases: dict[str, str] = {} + for match in MOBILE_ENTRY_RE.finditer(text): + parsed_aliases = re.findall(r'"([^"]+)"', match.group("aliases")) + if not parsed_aliases: + continue + canonical = parsed_aliases[0] + colors[canonical] = parse_color(match) + for alias in parsed_aliases[1:]: + aliases[alias] = canonical + return colors, aliases def main() -> int: mac = mac_colors() - mobile = mobile_colors() + mobile, aliases = mobile_palette() failures: list[str] = [] for provider, mac_color in sorted(mac.items()): @@ -71,10 +102,20 @@ def main() -> int: failures.append( f"{provider}: Mac {mac_color!r} != mobile {mobile_color!r}") - extra = sorted(set(mobile) - set(mac) - {"chatgpt", "anthropic", "bailian", "vertex"}) + extra = sorted(set(mobile) - set(mac)) for provider in extra: failures.append(f"{provider}: mobile canonical alias has no Mac descriptor") + for alias, canonical in sorted(aliases.items()): + expected = MOBILE_ALIAS_TARGETS.get(alias) + if expected != canonical: + failures.append( + f"{alias}: mobile alias points to {canonical!r}, expected {expected!r}") + + missing_aliases = sorted(set(MOBILE_ALIAS_TARGETS) - set(aliases)) + for alias in missing_aliases: + failures.append(f"{alias}: expected mobile alias is missing") + if failures: print("ERROR: provider palette parity audit failed:", file=sys.stderr) for failure in failures: diff --git a/Scripts/lint.sh b/Scripts/lint.sh index c7e29026..eeef0180 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -182,6 +182,7 @@ check_codex_parser_hash() { } audit_customer_branding() { + python3 "${ROOT_DIR}/Scripts/audit_customer_branding.py" --self-test python3 "${ROOT_DIR}/Scripts/audit_customer_branding.py" } From 1a3eeda04c9b2b401e043fbcdbef9fd89f696ba9 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:25:55 -0400 Subject: [PATCH 4/4] Fix about attribution lint wrapping --- Sources/CodexBar/PreferencesAboutPane.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/PreferencesAboutPane.swift b/Sources/CodexBar/PreferencesAboutPane.swift index d23a5d81..6e5c0cd2 100644 --- a/Sources/CodexBar/PreferencesAboutPane.swift +++ b/Sources/CodexBar/PreferencesAboutPane.swift @@ -113,7 +113,9 @@ struct AboutPane: View { .foregroundStyle(.secondary) } - Text("QuotaKit by Columbus Labs. QuotaKit is built in part from CodexBar by Peter Steinberger, licensed under the MIT License.") + Text( + "QuotaKit by Columbus Labs. " + + "QuotaKit is built in part from CodexBar by Peter Steinberger, licensed under the MIT License.") .font(.footnote) .foregroundStyle(.secondary) .padding(.top, 4)