diff --git a/Sources/CodeCaps/GlanceViews.swift b/Sources/CodeCaps/GlanceViews.swift index ca2dbc2e..577d4047 100644 --- a/Sources/CodeCaps/GlanceViews.swift +++ b/Sources/CodeCaps/GlanceViews.swift @@ -1378,40 +1378,23 @@ func glanceCadence(_ snapshot: QuotaWindowSnapshot, now: Date) -> GlanceCadence /// the caption column never has to truncate, and derived from the window's own /// cadence token rather than its label alone, because a label is allowed to be func isMuseAssistExtra(_ snapshot: QuotaWindowSnapshot) -> Bool { - let key = snapshot.window.canonicalProviderKey - guard key == "muse-assist" else { return false } - let label = snapshot.window.label - if QuotaWindowVocabulary.isExtraAllowanceLabel(label) { return true } - let lower = label.lowercased() - if lower.contains("additional") || lower.contains("extra") { return true } - // Only lean on the vocabulary's "Other limit" catch-all when the window - // carries no cadence token: a token-bearing window with a non-standard - // cadence (8h, 12h) also resolves to "Other limit", and reading that as - // an extra allowance would caption a real meter "+". - guard snapshot.window.window == nil else { return false } - let cadence = QuotaWindowVocabulary.standardCadence( - windowToken: nil, - label: label, + QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: snapshot.window.canonicalProviderKey, + windowToken: snapshot.window.window, + label: snapshot.window.label, resetAt: snapshot.window.resetDate, periodStart: snapshot.window.periodStartDate ) - return cadence == QuotaWindowVocabulary.otherLimit } func isMuseAssistWeekly(_ snapshot: QuotaWindowSnapshot) -> Bool { - let key = snapshot.window.canonicalProviderKey - guard key == "muse-assist" else { return false } - if isMuseAssistExtra(snapshot) { return false } - let cadence = QuotaWindowVocabulary.standardCadence( + QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: snapshot.window.canonicalProviderKey, windowToken: snapshot.window.window, label: snapshot.window.label, resetAt: snapshot.window.resetDate, periodStart: snapshot.window.periodStartDate ) - if cadence == QuotaWindowVocabulary.sevenDay { return true } - let label = snapshot.window.label.lowercased() - let token = (snapshot.window.window ?? "").lowercased() - return label.contains("weekly") || token.contains("weekly") || label.contains("free") } /// The unit tag beside a meter: "5h", "7d", "1m". Kept to five characters so diff --git a/Sources/CodeCaps/MonitorModel.swift b/Sources/CodeCaps/MonitorModel.swift index 719f78b2..d78bd8fc 100644 --- a/Sources/CodeCaps/MonitorModel.swift +++ b/Sources/CodeCaps/MonitorModel.swift @@ -156,12 +156,18 @@ final class MonitorModel: ObservableObject { didSet { defaults.set(menuBarStyle.rawValue, forKey: "menuBarStyle") } } @Published var menuBarMarkStyle: MenuBarMarkStyle { - didSet { defaults.set(menuBarMarkStyle.rawValue, forKey: "menuBarMarkStyle") } + didSet { + defaults.set(menuBarMarkStyle.rawValue, forKey: "menuBarMarkStyle") + invalidateProviderMarkCaches() + } } /// The accent colour. Stored through `AccentChoice` so the swatch, the /// default and the `Theme` lookup all read the same value. @Published var accent: AccentChoice { - didSet { AccentChoice.current = accent } + didSet { + AccentChoice.current = accent + invalidateProviderMarkCaches() + } } /// The accent as a drawable colour, resolved the same way `Theme.accent` @@ -1466,13 +1472,21 @@ final class MonitorModel: ObservableObject { /// theme. Offered as a switch because some people do want the popover to /// read as one piece. @Published var glanceLogosFollowAccent: Bool { - didSet { defaults.set(glanceLogosFollowAccent, forKey: "glanceLogosFollowAccent") } + didSet { + defaults.set(glanceLogosFollowAccent, forKey: "glanceLogosFollowAccent") + invalidateProviderMarkCaches() + } + } + + private func invalidateProviderMarkCaches() { + PlatformLogoImage.invalidateCaches() } func setMarkStyle(_ style: MarkStyle, for providerKey: String) { let key = providerKey.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard markStyles[key] != style else { return } markStyles[key] = style + invalidateProviderMarkCaches() } func customMarkMode(for providerKey: String) -> CustomMarkMode { diff --git a/Sources/CodeCaps/Resources/ProviderMarks/muse-code.png b/Sources/CodeCaps/Resources/ProviderMarks/muse-code.png index 010e8812..819ddf6d 100644 Binary files a/Sources/CodeCaps/Resources/ProviderMarks/muse-code.png and b/Sources/CodeCaps/Resources/ProviderMarks/muse-code.png differ diff --git a/Sources/QuotaCore/QuotaWindowVocabulary.swift b/Sources/QuotaCore/QuotaWindowVocabulary.swift index 7af59abe..3938dc85 100644 --- a/Sources/QuotaCore/QuotaWindowVocabulary.swift +++ b/Sources/QuotaCore/QuotaWindowVocabulary.swift @@ -243,4 +243,53 @@ public enum QuotaWindowVocabulary { return cadenceFromProse(lower, resetAt: nil, periodStart: nil, monthlyHint: false) != nil || ResetAlarmCadence.durationSeconds(lower) != nil } + + // MARK: - Muse Assist meter pairing + + /// Whether a Muse Assist window is the referral / extra allowance meter + /// (caption `+`), not a timed cadence such as an 8-hour window. + public static func isMuseAssistExtra( + providerKey: String, + windowToken: String?, + label: String, + resetAt: Date? = nil, + periodStart: Date? = nil + ) -> Bool { + guard providerKey == "muse-assist" else { return false } + if isExtraAllowanceLabel(label) { return true } + let lower = label.lowercased() + if lower.contains("additional") || lower.contains("extra") { return true } + guard windowToken == nil else { return false } + let cadence = standardCadence( + windowToken: nil, + label: label, + resetAt: resetAt, + periodStart: periodStart + ) + return cadence == otherLimit + } + + /// The Muse Assist weekly allowance meter (caption `7d`), distinct from + /// `isMuseAssistExtra`. + public static func isMuseAssistWeekly( + providerKey: String, + windowToken: String?, + label: String, + resetAt: Date? = nil, + periodStart: Date? = nil + ) -> Bool { + guard providerKey == "muse-assist" else { return false } + if isMuseAssistExtra(providerKey: providerKey, windowToken: windowToken, label: label, + resetAt: resetAt, periodStart: periodStart) { return false } + let cadence = standardCadence( + windowToken: windowToken, + label: label, + resetAt: resetAt, + periodStart: periodStart + ) + if cadence == sevenDay { return true } + let lower = label.lowercased() + let token = (windowToken ?? "").lowercased() + return lower.contains("weekly") || token.contains("weekly") || lower.contains("free") + } } diff --git a/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift b/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift index d3b0317f..083b5ffd 100644 --- a/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift +++ b/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift @@ -67,4 +67,27 @@ final class QuotaWindowVocabularyTests: XCTestCase { XCTAssertTrue(QuotaWindowVocabulary.isExtraAllowanceLabel("Additional additional tokens")) XCTAssertTrue(QuotaWindowVocabulary.isExtraAllowanceLabel("plan + extra")) } + + func testMuseAssistWeeklyAndExtraMetersAreClassifiedSeparately() { + XCTAssertTrue(QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: "muse-assist", + windowToken: "168h", + label: "Free weekly limit" + )) + XCTAssertTrue(QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: "muse-assist", + windowToken: nil, + label: "Additional tokens" + )) + XCTAssertFalse(QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: "muse-assist", + windowToken: "8h", + label: "8h window" + )) + XCTAssertFalse(QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: "openai", + windowToken: "168h", + label: "Weekly" + )) + } } diff --git a/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift b/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift index fc33076a..e6d5b370 100644 --- a/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift +++ b/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift @@ -156,10 +156,10 @@ public struct CompanionWindowItem: Identifiable, Codable, Equatable { FleetMacReading.isMacCodeCapsReading(id: id, source: source, via: via) } - /// Hyphenated cadence beside a meter bar (`5-hour`, `7-day`, `1-month`, `Other limit`). + /// Compact cadence beside a meter bar (`5h`, `7d`, `1m`, `+`), matching Mac Glance. public var meterCaption: String { - QuotaWindowVocabulary.standardCadence( - windowToken: cadence, + QuotaWindowVocabulary.popoverCaption( + windowToken: cadence.isEmpty ? nil : cadence, label: label, resetAt: resetAt, monthlyHint: label.lowercased().contains("plan") || cadence.lowercased().contains("plan") @@ -219,7 +219,7 @@ public struct CompanionWindowItem: Identifiable, Codable, Equatable { /// Countdown or a placeholder when an extra allowance has no reset clock. public func meterCountdown(now: Date = Date()) -> String { - if QuotaWindowVocabulary.isExtraAllowanceLabel(label) { return "—" } + if QuotaWindowVocabulary.isExtraAllowanceLabel(label) { return "extra" } let cd = countdown(now: now) return cd.isEmpty ? "—" : cd } @@ -398,12 +398,36 @@ public struct CompanionQuotaItem: Identifiable, Codable, Equatable { public func cardShortWindow(scope: CompanionSourceScope, now: Date = Date()) -> CardWindow? { let resolved = cardWindowRows(scope: scope, now: now) + if providerKey == "muse-assist" { + if let weekly = resolved.first(where: { + QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: providerKey, + windowToken: nil, + label: $0.window.label, + resetAt: $0.window.resetAt + ) + }) { + return weekly + } + } let window = resolved.map(\.window).first(where: { $0.isShortCadence }) ?? resolved.first?.window return resolved.first(where: { $0.window.id == window?.id }) } public func cardLongWindow(scope: CompanionSourceScope, now: Date = Date()) -> CardWindow? { let resolved = cardWindowRows(scope: scope, now: now) + if providerKey == "muse-assist" { + if let extra = resolved.first(where: { + QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: providerKey, + windowToken: nil, + label: $0.window.label, + resetAt: $0.window.resetAt + ) + }) { + return extra + } + } let short = cardShortWindow(scope: scope, now: now)?.window let nonShort = resolved.filter { !$0.window.isShortCadence } if let match = nonShort.first(where: { $0.window.id != short?.id }) { @@ -1338,6 +1362,18 @@ public final class CompanionQuotaModel: ObservableObject { // Standardize cadence order: shorter periods (e.g. 5-hour) precede longer periods (e.g. weekly). primaryWindows.sort { left, right in + if providerKey == "muse-assist" { + let leftWeekly = QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: providerKey, windowToken: nil, label: left.label, resetAt: left.resetAt) + let rightWeekly = QuotaWindowVocabulary.isMuseAssistWeekly( + providerKey: providerKey, windowToken: nil, label: right.label, resetAt: right.resetAt) + if leftWeekly != rightWeekly { return leftWeekly } + let leftExtra = QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: providerKey, windowToken: nil, label: left.label, resetAt: left.resetAt) + let rightExtra = QuotaWindowVocabulary.isMuseAssistExtra( + providerKey: providerKey, windowToken: nil, label: right.label, resetAt: right.resetAt) + if leftExtra != rightExtra { return !leftExtra } + } if left.isShortCadence != right.isShortCadence { return left.isShortCadence && !right.isShortCadence } diff --git a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code.png b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code.png index 106278db..a6345080 100644 Binary files a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code.png and b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code.png differ diff --git a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@2x.png b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@2x.png index b9a580c6..ca5daf97 100644 Binary files a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@2x.png and b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@2x.png differ diff --git a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@3x.png b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@3x.png index 5c802fa0..e5a4f806 100644 Binary files a/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@3x.png and b/ios/CodeCapsCompanion/Assets.xcassets/provider-muse-code.imageset/provider-muse-code@3x.png differ diff --git a/ios/CodeCapsCompanion/Widgets/WidgetPresentation.swift b/ios/CodeCapsCompanion/Widgets/WidgetPresentation.swift index 3e4d518a..d52beb8c 100644 --- a/ios/CodeCapsCompanion/Widgets/WidgetPresentation.swift +++ b/ios/CodeCapsCompanion/Widgets/WidgetPresentation.swift @@ -779,7 +779,7 @@ public enum WidgetPresentation { return ("muse-code", "Muse Code", "muse-code") } if pKey.contains("muse") || prov.contains("muse") { - return ("muse-code", "Muse Code", "muse-code") + return ("muse-assist", "Muse Assist", "muse-assist") } if pKey.contains("grok-bot") || prov.contains("grok-bot") || prov.contains("grok bot") || id.contains("grok-bot") { return ("grok-bot", "Grok Bot", "grok-bot") diff --git a/ios/CodeCapsCompanion/Widgets/WidgetViews.swift b/ios/CodeCapsCompanion/Widgets/WidgetViews.swift index 49bb9d3b..5e0f1a94 100644 --- a/ios/CodeCapsCompanion/Widgets/WidgetViews.swift +++ b/ios/CodeCapsCompanion/Widgets/WidgetViews.swift @@ -141,7 +141,7 @@ struct ProviderMarkView: View { if low.contains("minimax") { return "provider-minimax" } if low.contains("muse-assist") || low.contains("muse_assist") { return "provider-muse-assist" } if low.contains("muse-code") || low.contains("muse_code") { return "provider-muse-code" } - if low.contains("muse") { return "provider-muse-code" } + if low.contains("muse") { return "provider-muse-assist" } if low.contains("antigravity") { return "provider-antigravity" } if low.contains("gemini") { return "provider-gemini" } if low.contains("grok-bot") { return "provider-grok-bot" }