From ff070e4fe32bc1ae7f8beb6de9101c19971dd5c6 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:09:51 -0500 Subject: [PATCH 1/5] Responsive PiP countdown degradation, Muse Assist extra label, and xAI provider entry - Collapse platform names in PiP to logos only before dropping remaining time countdowns. - In space-constrained PiP sizes, degrade countdowns to single rounded-up time units (e.g. 1.2h -> 2h, 59m -> 59m, 2d 2h -> 3d). - Display 'extra' in place of countdown for Muse Assist extra usage bar to maintain uniform quota bar widths. - Suppress 'Updated...' timestamp strings for Muse Assist in tooltips and speech. - Add official xAI logo assets and register xAI provider entry. --- Sources/CodeCaps/GlanceViews.swift | 33 +++++- Sources/CodeCaps/PipWidget.swift | 94 +++++++++++++----- Sources/CodeCaps/PlatformLogo.swift | 5 +- .../Resources/ProviderMarks/xai-color.png | Bin 0 -> 6738 bytes .../Resources/ProviderMarks/xai-dark.png | Bin 0 -> 8426 bytes .../Resources/ProviderMarks/xai-light.png | Bin 0 -> 8577 bytes .../CodeCaps/Resources/ProviderMarks/xai.svg | 6 ++ Sources/QuotaCore/QuotaModels.swift | 6 +- Sources/QuotaCore/WidgetPlatformDisplay.swift | 3 +- Tests/CodeCapsTests/GlanceRowTests.swift | 17 ++++ Tests/CodeCapsTests/PipMetricsTests.swift | 79 ++++++++++++++- .../PlatformLogoResourceTests.swift | 4 +- Tests/QuotaCoreTests/QuotaCoreTests.swift | 10 +- 13 files changed, 211 insertions(+), 46 deletions(-) create mode 100644 Sources/CodeCaps/Resources/ProviderMarks/xai-color.png create mode 100644 Sources/CodeCaps/Resources/ProviderMarks/xai-dark.png create mode 100644 Sources/CodeCaps/Resources/ProviderMarks/xai-light.png create mode 100644 Sources/CodeCaps/Resources/ProviderMarks/xai.svg diff --git a/Sources/CodeCaps/GlanceViews.swift b/Sources/CodeCaps/GlanceViews.swift index 43ce6eb6..9c7b3f43 100644 --- a/Sources/CodeCaps/GlanceViews.swift +++ b/Sources/CodeCaps/GlanceViews.swift @@ -1619,13 +1619,16 @@ struct GlanceMeter: View { return glanceResetCountdown(snapshot.resetAt, now: now) } + private var isMuseAssist: Bool { snapshot.window.canonicalProviderKey == "muse-assist" } + /// The tooltip for a stale meter that has no reset to count down to. /// /// The countdown slot is empty then, so its own tooltip (which carries the /// "updated h:mm" staleness) has nothing to hang on; the bar carries it - /// instead, and fresh meters keep an empty tooltip. + /// instead, and fresh meters keep an empty tooltip. Muse Assist never + /// displays "Updated..." captions (owner 2026-10-17). private var staleBarHelp: String { - guard countdown.isEmpty else { return "" } + guard countdown.isEmpty, !isMuseAssist else { return "" } return glanceResetHelp(nil, now: now, observedAt: snapshot.observedAt, isFresh: snapshot.isFresh) ?? "" } @@ -1689,7 +1692,7 @@ struct GlanceMeter: View { // variable-width value cannot be centred in a trailing // frame. .frame(width: Metrics.glanceMeterCountdownWidth, alignment: .center) - .help(isExtraLabel ? "Extra token usage" : (glanceResetHelp(snapshot.resetAt, now: now, observedAt: snapshot.observedAt, isFresh: snapshot.isFresh) ?? "")) + .help(isExtraLabel ? "Extra token usage" : (glanceResetHelp(snapshot.resetAt, now: now, observedAt: isMuseAssist ? nil : snapshot.observedAt, isFresh: snapshot.isFresh) ?? "")) } else { Color.clear .frame(width: Metrics.glanceMeterCountdownWidth, height: 1) @@ -1710,7 +1713,7 @@ struct GlanceMeter: View { if !countdown.isEmpty { parts.append("resets in \(glanceResetFullCountdown(snapshot.resetAt, now: now))") } - if !snapshot.isFresh, let observed = snapshot.observedAt { + if !isMuseAssist, !snapshot.isFresh, let observed = snapshot.observedAt { parts.append("updated \(observed.formatted(date: .omitted, time: .shortened))") } return parts.joined(separator: ", ") @@ -2429,6 +2432,28 @@ func glanceResetCountdown(_ reset: Date?, now: Date) -> String { return glanceCountdownUnits(seconds: seconds).prefix(2).joined(separator: " ") } +/// Reset countdown in a single unit rounded up: "2h", "59m", "3d", "<1m", "due". +/// +/// When the PiP HUD narrows to where two time units cannot fit, it degrades to +/// one unit rounded up rather than dropping the countdown entirely: +/// 1.2h (1h 12m) -> 2h, 59m -> 59m, 2d 2h -> 3d. +func glanceResetCompactCountdown(_ reset: Date?, now: Date) -> String { + guard let reset else { return "" } + let seconds = Int(reset.timeIntervalSince(now).rounded()) + guard seconds > 0 else { return "due" } + guard seconds >= 60 else { return "<1m" } + let minutes = seconds / 60 + if minutes < 60 { + return "\(minutes)m" + } else if minutes < 1_440 { + let hours = (minutes + 59) / 60 + return "\(hours)h" + } else { + let days = (minutes + 1_439) / 1_440 + return "\(days)d" + } +} + /// The whole countdown, down to the minute, with zero units skipped the same /// way: "4d 2h 42m", "2h 42m", "2h", "45m". func glanceResetFullCountdown(_ reset: Date?, now: Date) -> String { diff --git a/Sources/CodeCaps/PipWidget.swift b/Sources/CodeCaps/PipWidget.swift index e0708e25..dc16db41 100644 --- a/Sources/CodeCaps/PipWidget.swift +++ b/Sources/CodeCaps/PipWidget.swift @@ -31,6 +31,7 @@ enum PipMetrics { /// at the narrow end when it was right-aligned into a tight frame. static let percentWidth: CGFloat = 30 static let countdownWidth: CGFloat = 42 + static let countdownCompactWidth: CGFloat = 28 static let barMinWidth: CGFloat = 20 static let barHeight: CGFloat = 4 @@ -64,6 +65,10 @@ enum PipMetrics { static let meterMinWidthWithCountdown: CGFloat = meterMinWidth + meterSpacing + countdownWidth + /// The same meter plus its compact reset countdown. + static let meterMinWidthWithCompactCountdown: CGFloat = + meterMinWidth + meterSpacing + countdownCompactWidth + /// Caption-less meter: bar and percent only. private static let bareMeterMinWidth: CGFloat = meterSpacing + barMinWidth + meterSpacing + percentWidth @@ -73,15 +78,23 @@ enum PipMetrics { providerLogoSize + rowSpacing + titleWidth + rowSpacing + meterMinWidthWithCountdown + rowSpacing + meterMinWidthWithCountdown - /// Provider logo, title and two meters, countdowns dropped. - static let noCountdownMinWidth: CGFloat = - providerLogoSize + rowSpacing + titleWidth + rowSpacing - + meterMinWidth + rowSpacing + meterMinWidth + /// Provider logo and two meters with 2-unit countdowns; platform name dropped first. + static let logoWithCountdownMinWidth: CGFloat = + providerLogoSize + rowSpacing + + meterMinWidthWithCountdown + rowSpacing + meterMinWidthWithCountdown + + /// Provider logo and two meters with 1-unit rounded countdowns; platform name dropped. + static let logoWithCompactCountdownMinWidth: CGFloat = + providerLogoSize + rowSpacing + + meterMinWidthWithCompactCountdown + rowSpacing + meterMinWidthWithCompactCountdown - /// Provider logo and two meters, no countdowns and no title (platform name dropped first). + /// Provider logo and two meters, countdowns and title dropped (platform name dropped first). static let logoOnlyMinWidth: CGFloat = providerLogoSize + rowSpacing + meterMinWidth + rowSpacing + meterMinWidth + /// Legacy alias so any existing reference compiles cleanly. + static let noCountdownMinWidth: CGFloat = logoOnlyMinWidth + /// Two labelled meters, with no provider identity at all. static let minimalMinWidth: CGFloat = meterMinWidth + rowSpacing + meterMinWidth @@ -100,14 +113,16 @@ enum PipMetrics { /// How much of a row survives at a given panel width, richest first. /// - /// The platform name is dropped before the second bar: - /// `full > noCountdown > logoOnly > minimal > singleMeter > barsOnly > singleBar`. + /// The platform name is dropped before the countdown disappears: + /// `full > logoWithFullCountdown > logoWithCompactCountdown > logoOnly > minimal > singleMeter > barsOnly > singleBar`. enum Detail: Int, CaseIterable, Comparable { - /// Bar, percent, reset countdown; provider logo and title. + /// Bar, percent, 2-unit reset countdown; provider logo and title. case full - /// Two meters, no countdowns. - case noCountdown - /// Two meters and the provider's logo, but platform name dropped. + /// Two meters with 2-unit countdowns; provider logo kept, platform name dropped. + case logoWithFullCountdown + /// Two meters with 1-unit rounded-up countdowns; provider logo kept, platform name dropped. + case logoWithCompactCountdown + /// Two meters, countdowns and platform name dropped; provider logo kept. case logoOnly /// Two labelled meters, provider identity dropped entirely. case minimal @@ -118,17 +133,24 @@ enum PipMetrics { /// One bar and its percentage. case singleBar + /// Legacy alias for tests. + static let noCountdown = Detail.logoOnly + static func < (lhs: Detail, rhs: Detail) -> Bool { lhs.rawValue > rhs.rawValue } var maxMeters: Int { switch self { - case .full, .noCountdown, .logoOnly, .minimal, .barsOnly: return 2 + case .full, .logoWithFullCountdown, .logoWithCompactCountdown, .logoOnly, .minimal, .barsOnly: return 2 case .singleMeter, .singleBar: return 1 } } var showsCountdown: Bool { - self == .full || self == .singleMeter + self == .full || self == .logoWithFullCountdown || self == .logoWithCompactCountdown || self == .singleMeter + } + + var usesCompactCountdown: Bool { + self == .logoWithCompactCountdown } var showsCadence: Bool { @@ -136,12 +158,12 @@ enum PipMetrics { } var showsProviderLogo: Bool { - self == .full || self == .noCountdown || self == .logoOnly || self == .singleMeter + self == .full || self == .logoWithFullCountdown || self == .logoWithCompactCountdown || self == .logoOnly || self == .singleMeter } - /// The platform name drops before the second bar. + /// The platform name drops before the countdown: only `.full` keeps the title. var showsTitle: Bool { - self == .full || self == .noCountdown + self == .full } } @@ -151,7 +173,8 @@ enum PipMetrics { let available = width - padding * 2 switch available { case fullMinWidth...: return .full - case noCountdownMinWidth...: return .noCountdown + case logoWithCountdownMinWidth...: return .logoWithFullCountdown + case logoWithCompactCountdownMinWidth...: return .logoWithCompactCountdown case logoOnlyMinWidth...: return .logoOnly case minimalMinWidth...: return .minimal case singleMeterMinWidth...: return .singleMeter @@ -178,10 +201,11 @@ enum PipMetrics { let base: CGFloat switch detail { case .full: base = fullMinWidth - case .noCountdown: base = noCountdownMinWidth - case .singleMeter: base = singleMeterMinWidth + case .logoWithFullCountdown: base = logoWithCountdownMinWidth + case .logoWithCompactCountdown: base = logoWithCompactCountdownMinWidth case .logoOnly: base = logoOnlyMinWidth case .minimal: base = minimalMinWidth + case .singleMeter: base = singleMeterMinWidth case .barsOnly: base = barsOnlyMinWidth case .singleBar: base = singleMeterMinWidth } @@ -864,10 +888,12 @@ struct PipWidgetView: View { private func detailWindowRow(_ snapshot: QuotaWindowSnapshot, detail: PipMetrics.Detail) -> some View { let metrics = QuotaBarMetrics(snapshot: snapshot, now: model.now) let remaining = snapshot.remainingPercent - let cd = glanceResetCountdown(snapshot.resetAt, now: model.now) + let isExtra = isMuseAssistExtra(snapshot) + let cd = isExtra ? "extra" : (detail.usesCompactCountdown ? glanceResetCompactCountdown(snapshot.resetAt, now: model.now) : glanceResetCountdown(snapshot.resetAt, now: model.now)) let showExpanded = detail == .full let cadenceCaption = showExpanded ? singleModelCadenceExpanded(snapshot) : glanceMeterCaption(snapshot) let cleanLabel = cleanWindowLabel(snapshot.window.label.isEmpty ? "Quota" : snapshot.window.label) + let countdownWidth = detail.usesCompactCountdown ? PipMetrics.countdownCompactWidth : PipMetrics.countdownWidth return VStack(alignment: .leading, spacing: 1) { HStack(spacing: 5) { @@ -886,9 +912,11 @@ struct PipWidgetView: View { .fixedSize() if !cd.isEmpty { Text(cd) - .font(.system(size: 9).monospacedDigit()) - .foregroundStyle(background.countdownInk) + .font(isExtra ? .system(size: 9, weight: .bold) : .system(size: 9).monospacedDigit()) + .foregroundStyle(isExtra ? background.ink : background.countdownInk) .lineLimit(1) + .minimumScaleFactor(0.75) + .frame(width: countdownWidth, alignment: .trailing) } } QuotaUsageBar( @@ -899,7 +927,7 @@ struct PipWidgetView: View { ) .frame(height: 5) } - .help(snapshot.resetAt.map { "Resets \(glanceResetCountdown($0, now: model.now))" } ?? "") + .help(isExtra ? "Extra token usage" : (snapshot.resetAt.map { "Resets \(glanceResetCountdown($0, now: model.now))" } ?? "")) } /// The HUD's capped state, in place of its bars: the words "At Cap" and @@ -922,10 +950,19 @@ struct PipWidgetView: View { private func pipMeter(_ snapshot: QuotaWindowSnapshot, detail: PipMetrics.Detail) -> some View { let metrics = QuotaBarMetrics(snapshot: snapshot, now: model.now) - let cd = glanceResetCountdown(snapshot.resetAt, now: model.now) + let isExtra = isMuseAssistExtra(snapshot) + let cd: String + if isExtra { + cd = "extra" + } else if detail.usesCompactCountdown { + cd = glanceResetCompactCountdown(snapshot.resetAt, now: model.now) + } else { + cd = glanceResetCountdown(snapshot.resetAt, now: model.now) + } let remaining = snapshot.remainingPercent let showExpanded = isSingleModel && detail == .full let cadenceCaption = showExpanded ? singleModelCadenceExpanded(snapshot) : glanceMeterCaption(snapshot) + let countdownWidth = detail.usesCompactCountdown ? PipMetrics.countdownCompactWidth : PipMetrics.countdownWidth return HStack(spacing: PipMetrics.meterSpacing) { if detail.showsCadence { @@ -953,11 +990,14 @@ struct PipWidgetView: View { if detail.showsCountdown, !cd.isEmpty { Text(cd) - .font(.system(size: 9, weight: .medium).monospacedDigit()) - .foregroundStyle(background.countdownInk) + .font(isExtra + ? .system(size: 9, weight: .bold) + : .system(size: 9, weight: .medium).monospacedDigit()) + .foregroundStyle(isExtra ? background.ink : background.countdownInk) .lineLimit(1) + .minimumScaleFactor(0.75) .fixedSize() - .frame(width: PipMetrics.countdownWidth, alignment: .trailing) + .frame(width: countdownWidth, alignment: .trailing) } } .fixedSize(horizontal: false, vertical: true) diff --git a/Sources/CodeCaps/PlatformLogo.swift b/Sources/CodeCaps/PlatformLogo.swift index 719c8cf9..c08a135b 100644 --- a/Sources/CodeCaps/PlatformLogo.swift +++ b/Sources/CodeCaps/PlatformLogo.swift @@ -280,7 +280,8 @@ private static let resourceNames: [String: String] = [ "google-antigravity:gemini": "gemini-color", "google-antigravity:third-party": "antigravity", "gemini": "gemini", - "xai": "grok", + "xai": "xai", + "x-ai": "xai", "grok": "grok", "grok-cli": "grok", "grok-bot": "grok-bot", @@ -484,7 +485,7 @@ private static func variantFile(base: String, style: MarkStyle, isDark: Bool) -> private static let monochromeKeys: Set = [ "grok-bot", - "openai", "codex", "cursor", "xai", "grok", "grok-cli", + "openai", "codex", "cursor", "xai", "x-ai", "grok", "grok-cli", ] /// Presentation mode for a user-supplied custom mark. Defaults to `.color` diff --git a/Sources/CodeCaps/Resources/ProviderMarks/xai-color.png b/Sources/CodeCaps/Resources/ProviderMarks/xai-color.png new file mode 100644 index 0000000000000000000000000000000000000000..4a80657641f5cd824d49630372d5cacb3694c2c7 GIT binary patch literal 6738 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 literal 0 HcmV?d00001 diff --git a/Sources/CodeCaps/Resources/ProviderMarks/xai-dark.png b/Sources/CodeCaps/Resources/ProviderMarks/xai-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d33fb870ca70b9cda465bb0e98b7e761ddfa7b GIT binary patch literal 8426 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 + + + + + diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index deab8f2c..c0f453ac 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -778,7 +778,8 @@ enum QuotaProviders { Expected(key: "openai", label: "Codex", via: nil), Expected(key: "google-antigravity", label: "Antigravity", via: "antigravity"), Expected(key: "cursor", label: "Cursor", via: nil), - Expected(key: "xai", label: "Grok", via: nil), + Expected(key: "xai", label: "xAI", via: nil), + Expected(key: "grok", label: "Grok", via: nil), Expected(key: "grok-bot", label: "Grok Bot", via: "cursor"), Expected(key: "minimax", label: "MiniMax", via: nil), Expected(key: "muse-assist", label: "Muse Assist", via: nil), @@ -804,7 +805,8 @@ enum QuotaProviders { "openai": "openai", "openai-codex": "openai", "codex": "openai", "google": "google-antigravity", "google-antigravity": "google-antigravity", "antigravity": "google-antigravity", "antigravity-cli": "google-antigravity", "gemini": "google-antigravity", "cursor": "cursor", - "xai": "xai", "grok": "xai", "grok-build": "xai", + "xai": "xai", "x-ai": "xai", "grok-build": "xai", + "grok": "grok", "grok-cli": "grok", "grok-bot": "grok-bot", "grok bot": "grok-bot", "grokbot": "grok-bot", "minimax": "minimax", "minimax-code": "minimax", "muse-assist": "muse-assist", "muse assist": "muse-assist", "muse_assist": "muse-assist", "muse-assistant": "muse-assist", "muse assistant": "muse-assist", diff --git a/Sources/QuotaCore/WidgetPlatformDisplay.swift b/Sources/QuotaCore/WidgetPlatformDisplay.swift index 52916996..99667f9a 100644 --- a/Sources/QuotaCore/WidgetPlatformDisplay.swift +++ b/Sources/QuotaCore/WidgetPlatformDisplay.swift @@ -10,7 +10,8 @@ public enum WidgetPlatformDisplay { "openai": "Codex", "google-antigravity": "Antigravity", "cursor": "Cursor", - "xai": "Grok", + "xai": "xAI", + "grok": "Grok", "grok-bot": "Grok Bot", "minimax": "MiniMax", "muse-assist": "Muse Assist", diff --git a/Tests/CodeCapsTests/GlanceRowTests.swift b/Tests/CodeCapsTests/GlanceRowTests.swift index 9bc4ecc9..961e05b7 100644 --- a/Tests/CodeCapsTests/GlanceRowTests.swift +++ b/Tests/CodeCapsTests/GlanceRowTests.swift @@ -899,6 +899,23 @@ final class GlanceMeterTests: XCTestCase { XCTAssertEqual(glanceResetCountdown(snapshot.resetAt, now: now), "4h 32m") } + func testMuseAssistNeverSaysUpdatedInTooltipOrSpeech() { + let formatter = ISO8601DateFormatter() + let staleObserved = now.addingTimeInterval(-45 * 60) + let w = QuotaWindow( + id: "weekly", provider: "muse-assist", label: "weekly window", + remainingPercent: 92, + resetAt: nil, + window: "weekly", + occurredAt: formatter.string(from: staleObserved) + ) + let snapshot = QuotaWindowSnapshot(window: w, now: now) + XCTAssertFalse(snapshot.isFresh) + // Help tooltip for muse-assist must not say "Updated..." + let help = glanceResetHelp(snapshot.resetAt, now: now, observedAt: snapshot.window.canonicalProviderKey == "muse-assist" ? nil : snapshot.observedAt, isFresh: snapshot.isFresh) + XCTAssertNil(help) + } + // MARK: Cadence func testCadenceReadsTheWindowTokenWhenPresent() { diff --git a/Tests/CodeCapsTests/PipMetricsTests.swift b/Tests/CodeCapsTests/PipMetricsTests.swift index e1d62c04..17cd3629 100644 --- a/Tests/CodeCapsTests/PipMetricsTests.swift +++ b/Tests/CodeCapsTests/PipMetricsTests.swift @@ -65,14 +65,87 @@ final class PipMetricsTests: XCTestCase { XCTAssertFalse(PipMetrics.Detail.minimal.showsProviderLogo, "the logo goes only after the name, at the identity-free end") - // Nothing above logoOnly may lose the name while keeping two meters. - for level in [PipMetrics.Detail.full, .noCountdown, .logoOnly] { + // Nothing above logoOnly may lose the logo while keeping two meters. + for level in [PipMetrics.Detail.full, .logoWithFullCountdown, .logoWithCompactCountdown, .logoOnly] { if level.maxMeters > 1 || level == .logoOnly { XCTAssertTrue(level.showsProviderLogo, "\(level) must keep the logo") } } } + /// Owner 2026-10-17: On the PiP window make the platform name disappear and + /// go to just the logos when it gets smaller instead of having the remaining + /// time disappear first. Then degrade the remaining time to a single rounded-up + /// unit before dropping countdowns entirely. + func testPlatformNameDropsBeforeCountdownAndDegradesToCompactUnit() { + let fullWidth = PipMetrics.padding * 2 + PipMetrics.fullMinWidth + XCTAssertEqual(PipMetrics.detail(forWidth: fullWidth), .full) + XCTAssertTrue(PipMetrics.detail(forWidth: fullWidth).showsTitle) + XCTAssertTrue(PipMetrics.detail(forWidth: fullWidth).showsCountdown) + XCTAssertFalse(PipMetrics.detail(forWidth: fullWidth).usesCompactCountdown) + + // Below full width: platform name disappears to logos only, but countdown remains! + let fullCountdownWidth = PipMetrics.padding * 2 + PipMetrics.logoWithCountdownMinWidth + let belowFull = PipMetrics.detail(forWidth: fullWidth - 1) + XCTAssertEqual(belowFull, .logoWithFullCountdown) + XCTAssertFalse(belowFull.showsTitle, "platform name must drop first") + XCTAssertTrue(belowFull.showsProviderLogo, "logos must stay") + XCTAssertTrue(belowFull.showsCountdown, "countdown must not disappear first") + XCTAssertFalse(belowFull.usesCompactCountdown) + + // Narrower still: countdown degrades to 1-unit rounded up. + let compactWidth = PipMetrics.padding * 2 + PipMetrics.logoWithCompactCountdownMinWidth + let compactDetail = PipMetrics.detail(forWidth: fullCountdownWidth - 1) + XCTAssertEqual(compactDetail, .logoWithCompactCountdown) + XCTAssertFalse(compactDetail.showsTitle) + XCTAssertTrue(compactDetail.showsProviderLogo) + XCTAssertTrue(compactDetail.showsCountdown) + XCTAssertTrue(compactDetail.usesCompactCountdown, "must use compact single-unit countdown") + + // Narrower still: countdowns drop, logo remains. + let logoOnlyDetail = PipMetrics.detail(forWidth: compactWidth - 1) + XCTAssertEqual(logoOnlyDetail, .logoOnly) + XCTAssertFalse(logoOnlyDetail.showsTitle) + XCTAssertTrue(logoOnlyDetail.showsProviderLogo) + XCTAssertFalse(logoOnlyDetail.showsCountdown, "countdowns drop after compact countdown") + } + + /// Owner 2026-10-17: "when it gets small enough that it can't have the time + /// remaining with 2 types of time (days hours or hours minutes) then lets first + /// go down to just one type of time rounded up so 1.2 hours (1hr 12min) would + /// say 2h and 59min would say 59min and 2d 2h would say 3d". + func testCompactCountdownRoundsUpToSingleUnit() { + let now = Date() + + // 1.2 hours (1hr 12min = 72m) -> 2h + let onePointTwoHours = now.addingTimeInterval(72 * 60) + XCTAssertEqual(glanceResetCompactCountdown(onePointTwoHours, now: now), "2h") + + // 59 minutes -> 59m + let fiftyNineMins = now.addingTimeInterval(59 * 60) + XCTAssertEqual(glanceResetCompactCountdown(fiftyNineMins, now: now), "59m") + + // 2d 2h (3000 mins) -> 3d + let twoDaysTwoHours = now.addingTimeInterval((2 * 1440 + 2 * 60) * 60) + XCTAssertEqual(glanceResetCompactCountdown(twoDaysTwoHours, now: now), "3d") + + // Exactly 1 hour -> 1h + let oneHour = now.addingTimeInterval(60 * 60) + XCTAssertEqual(glanceResetCompactCountdown(oneHour, now: now), "1h") + + // Exactly 2 days -> 2d + let twoDays = now.addingTimeInterval(2 * 86_400) + XCTAssertEqual(glanceResetCompactCountdown(twoDays, now: now), "2d") + + // Sub-minute -> <1m + let subMinute = now.addingTimeInterval(45) + XCTAssertEqual(glanceResetCompactCountdown(subMinute, now: now), "<1m") + + // Expired -> due + let expired = now.addingTimeInterval(-10) + XCTAssertEqual(glanceResetCompactCountdown(expired, now: now), "due") + } + /// Owner 2026-10-10: "once it gets to where it is in the screenshot or /// actually 80-100 pixels wider than that so that it could still have /// platform logos at the left … we could make it scroll left to right like @@ -148,7 +221,7 @@ final class PipMetricsTests: XCTestCase { for title in ["Antigravity · Third-Party", "Antigravity · Gemini", "Muse Assist", "Muse Code", "MiniMax", "OpenCode"] { XCTAssertLessThanOrEqual((title as NSString).size(withAttributes: [.font: font]).width, PipMetrics.titleWidth) } - let boundary = PipMetrics.padding * 2 + PipMetrics.noCountdownMinWidth + let boundary = PipMetrics.padding * 2 + PipMetrics.fullMinWidth XCTAssertTrue(PipMetrics.detail(forWidth: boundary).showsTitle) XCTAssertFalse(PipMetrics.detail(forWidth: boundary - 1).showsTitle) XCTAssertTrue(PipMetrics.detail(forWidth: boundary - 1).showsProviderLogo) diff --git a/Tests/CodeCapsTests/PlatformLogoResourceTests.swift b/Tests/CodeCapsTests/PlatformLogoResourceTests.swift index 65117f0b..e6458190 100644 --- a/Tests/CodeCapsTests/PlatformLogoResourceTests.swift +++ b/Tests/CodeCapsTests/PlatformLogoResourceTests.swift @@ -58,7 +58,7 @@ final class PlatformLogoResourceTests: XCTestCase { let bundle = try XCTUnwrap(ResourceBundle.resolved, "the SwiftPM resource bundle could not be resolved at all") let marks = ["claude", "openai", "gemini", "gemini-color", "gemini-mono", - "cursor", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] + "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] // Every named mark must be present in some form. Deliberately not an // exact count: the owner replacing the fabricated MiniMax `{M}` with // the real PNG mark added a file, and an equality assertion is a test @@ -75,7 +75,7 @@ final class PlatformLogoResourceTests: XCTestCase { /// a different hat: `NSImage` returns an object, and the row draws nothing. func testEveryBundledMarkRasterises() throws { let bundle = try XCTUnwrap(ResourceBundle.resolved) - for name in ["claude", "openai", "gemini", "gemini-mono", "cursor", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] { + for name in ["claude", "openai", "gemini", "gemini-mono", "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] { for ext in ["svg", "png"] { guard let url = bundle.url(forResource: name, withExtension: ext) else { continue } let image = try XCTUnwrap(NSImage(contentsOf: url), "\(name).\(ext) did not load") diff --git a/Tests/QuotaCoreTests/QuotaCoreTests.swift b/Tests/QuotaCoreTests/QuotaCoreTests.swift index f371cc38..66b00103 100644 --- a/Tests/QuotaCoreTests/QuotaCoreTests.swift +++ b/Tests/QuotaCoreTests/QuotaCoreTests.swift @@ -85,8 +85,8 @@ final class QuotaCoreTests: XCTestCase { // bundled list. The list's order and labels must match. let response = QuotaResponse(generatedAt: "", windows: []) let sections = response.platformSections(now: now) - XCTAssertEqual(Array(sections.prefix(7)).map(\.providerKey), [ - "anthropic", "openai", "google-antigravity", "cursor", "xai", "grok-bot", "minimax", + XCTAssertEqual(Array(sections.prefix(8)).map(\.providerKey), [ + "anthropic", "openai", "google-antigravity", "cursor", "xai", "grok", "grok-bot", "minimax", ]) XCTAssertEqual(sections.first?.providerLabel, "Claude") XCTAssertNil(sections.first?.iconHint) @@ -202,10 +202,10 @@ final class QuotaCoreTests: XCTestCase { let future = window(provider: "new-provider", occurred: "2023-11-14T22:05:00Z") let response = QuotaResponse(generatedAt: "2023-11-14T22:10:00Z", windows: [future]) let sections = response.platformSections(now: now) - XCTAssertEqual(Array(sections.prefix(9)).map(\.providerKey), [ - "anthropic", "openai", "google-antigravity", "cursor", "xai", "grok-bot", "minimax", "muse-assist", "muse-code", + XCTAssertEqual(Array(sections.prefix(10)).map(\.providerKey), [ + "anthropic", "openai", "google-antigravity", "cursor", "xai", "grok", "grok-bot", "minimax", "muse-assist", "muse-code", ]) - XCTAssertTrue(sections.prefix(9).allSatisfy(\.isMissing)) + XCTAssertTrue(sections.prefix(10).allSatisfy(\.isMissing)) XCTAssertEqual(sections.last?.providerKey, "new-provider") } From afc4d059f9d23a12d7570112802fab2362800c75 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:11:18 -0500 Subject: [PATCH 2/5] fix: align local Grok identity with distinct provider entry --- Sources/QuotaCore/LocalQuotaReader.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/QuotaCore/LocalQuotaReader.swift b/Sources/QuotaCore/LocalQuotaReader.swift index 13c0c09b..fe1b4549 100644 --- a/Sources/QuotaCore/LocalQuotaReader.swift +++ b/Sources/QuotaCore/LocalQuotaReader.swift @@ -29,7 +29,7 @@ public struct LocalQuotaResult: Equatable, Sendable { /// only the four documented CLI files below and the single Antigravity helper /// path. Network requests use an ephemeral session and refuse redirects. public struct LocalQuotaReader: Sendable { - public static let providerKeys = ["anthropic", "openai", "xai", "minimax", "google-antigravity"] + public static let providerKeys = ["anthropic", "openai", "grok", "minimax", "google-antigravity"] private let homeDirectory: URL private let now: @Sendable () -> Date @@ -377,7 +377,7 @@ public struct LocalQuotaReader: Sendable { private enum Provider: String, CaseIterable, Sendable { case claude, codex, grok, minimax, antigravity var key: String { - switch self { case .claude: return "anthropic"; case .codex: return "openai"; case .grok: return "xai"; case .minimax: return "minimax"; case .antigravity: return "google-antigravity" } + switch self { case .claude: return "anthropic"; case .codex: return "openai"; case .grok: return "grok"; case .minimax: return "minimax"; case .antigravity: return "google-antigravity" } } var label: String { switch self { case .claude: return "Claude Code"; case .codex: return "Codex"; case .grok: return "Grok"; case .minimax: return "MiniMax"; case .antigravity: return "Antigravity" } From 19fb8da46c9710b78bc3dd171e8ce8fab4c5c0f8 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:11:39 -0500 Subject: [PATCH 3/5] test: cover local Grok provider separation --- .../LocalQuotaReaderTests.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Tests/QuotaCoreTests/LocalQuotaReaderTests.swift b/Tests/QuotaCoreTests/LocalQuotaReaderTests.swift index 8bf5a79e..8af76fae 100644 --- a/Tests/QuotaCoreTests/LocalQuotaReaderTests.swift +++ b/Tests/QuotaCoreTests/LocalQuotaReaderTests.swift @@ -81,11 +81,11 @@ final class LocalQuotaReaderTests: XCTestCase { ) let result = await reader.read() - XCTAssertEqual(result.windows.first(where: { $0.providerKey == "xai" })?.remainingPercent, 75) + XCTAssertEqual(result.windows.first(where: { $0.providerKey == "grok" })?.remainingPercent, 75) XCTAssertEqual(result.windows.first(where: { $0.providerKey == "minimax" })?.remainingPercent, 25) XCTAssertEqual(result.windows.first(where: { $0.providerKey == "google-antigravity" })?.remainingPercent, 25) XCTAssertEqual(result.windows.first(where: { $0.providerKey == "google-antigravity" })?.via, "antigravity") - XCTAssertNil(result.issues["xai"]) + XCTAssertNil(result.issues["grok"]) XCTAssertNil(result.issues["minimax"]) XCTAssertNil(result.issues["google-antigravity"]) } @@ -105,7 +105,10 @@ final class LocalQuotaReaderTests: XCTestCase { XCTAssertEqual(result.issues["anthropic"], ClaudeLoginState.signedOut.issue) XCTAssertFalse(result.consentNeeded.contains("anthropic")) XCTAssertEqual(result.issues["openai"], "Codex is not signed in locally.") - XCTAssertEqual(result.issues["xai"], "Grok is not signed in locally.") + XCTAssertEqual(result.issues["grok"], "Grok is not signed in locally.") + XCTAssertNil(result.issues["xai"]) + XCTAssertTrue(LocalQuotaReader.providerKeys.contains("grok")) + XCTAssertFalse(LocalQuotaReader.providerKeys.contains("xai")) XCTAssertFalse(result.issues.values.joined(separator: " ").contains("super-secret")) XCTAssertFalse(result.issues.values.joined(separator: " ").contains("account-secret")) XCTAssertFalse(result.issues.values.joined(separator: " ").contains("private")) @@ -164,7 +167,7 @@ final class LocalQuotaReaderTests: XCTestCase { }) let result = await reader.read() XCTAssertEqual(result.windows.first { $0.providerKey == "anthropic" }?.remainingPercent, 70) - XCTAssertEqual(result.windows.first { $0.providerKey == "xai" }?.remainingPercent, 40) + XCTAssertEqual(result.windows.first { $0.providerKey == "grok" }?.remainingPercent, 40) } func testClaudeUsageRejectionDropsTheRememberedGrantAndAsksAgain() async throws { @@ -346,8 +349,20 @@ final class LocalQuotaReaderTests: XCTestCase { Self.httpResponse(#"{"config":{"creditUsagePercent":51,"currentPeriod":{"start":"2026-09-10T00:00:00Z","end":"2026-09-17T00:00:00Z"},"onDemandCap":{"val":500},"prepaidBalance":{"val":300},"productUsage":[{"usagePercent":50},{"usagePercent":1}]}}"#) }, runAntigravity: { Data("{}".utf8) }) let result = await reader.read() - let grok = try XCTUnwrap(result.windows.first { $0.providerKey == "xai" }) + let grok = try XCTUnwrap(result.windows.first { $0.providerKey == "grok" }) + XCTAssertEqual(grok.id, "local-mac:grok:subscription") + XCTAssertEqual(grok.canonicalProviderKey, "grok") XCTAssertEqual(grok.remainingPercent, 49) + + let sections = QuotaResponse(generatedAt: "", windows: result.windows) + .platformSections(now: observedAt) + let grokSection = try XCTUnwrap(sections.first { $0.providerKey == "grok" }) + let xaiSection = try XCTUnwrap(sections.first { $0.providerKey == "xai" }) + XCTAssertEqual(grokSection.providerLabel, "Grok") + XCTAssertEqual(grokSection.windows.map { $0.window.id }, [grok.id]) + XCTAssertFalse(grokSection.isMissing) + XCTAssertEqual(xaiSection.providerLabel, "xAI") + XCTAssertTrue(xaiSection.isMissing) XCTAssertEqual(grok.window, "1w") XCTAssertEqual(grok.resetAt, "2026-09-17T00:00:00Z") XCTAssertNil(grok.absoluteLimit) From a64349075fc3150262f64643a20b3d991cd43fc9 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:11:43 -0500 Subject: [PATCH 4/5] test: cover local Grok provider separation --- Tests/CodeCapsTests/SettingsMigrationTests.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/CodeCapsTests/SettingsMigrationTests.swift b/Tests/CodeCapsTests/SettingsMigrationTests.swift index 6fbc489d..4b3e7267 100644 --- a/Tests/CodeCapsTests/SettingsMigrationTests.swift +++ b/Tests/CodeCapsTests/SettingsMigrationTests.swift @@ -37,6 +37,13 @@ final class SettingsMigrationTests: XCTestCase { } } + func testGrokReaderStatusUsesTheLocalGrokProviderKey() throws { + let reader = try XCTUnwrap(ReaderStatus.all.first { $0.source == "Grok CLI credentials" }) + XCTAssertEqual(reader.providerKey, "grok") + XCTAssertEqual(reader.label, "Grok") + XCTAssertFalse(ReaderStatus.all.contains { $0.providerKey == "xai" }) + } + func testFreshInstallPostsNowhere() { let model = MonitorModel(defaults: defaults) XCTAssertEqual(model.endpoint, "") From 3e407a0ed00d8ca669c60993606ed3d0fab063b2 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:11:53 -0500 Subject: [PATCH 5/5] fix: align local Grok identity with distinct provider entry --- Sources/CodeCaps/SettingsViews.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CodeCaps/SettingsViews.swift b/Sources/CodeCaps/SettingsViews.swift index bc6c0436..ac46b22e 100644 --- a/Sources/CodeCaps/SettingsViews.swift +++ b/Sources/CodeCaps/SettingsViews.swift @@ -942,7 +942,7 @@ struct ReaderStatus { ReaderStatus(providerKey: "openai", label: "Codex", source: "Codex CLI credentials"), ReaderStatus(providerKey: "google-antigravity", label: "Antigravity", source: "Antigravity app or CLI"), ReaderStatus(providerKey: "cursor", label: "Cursor", source: "Cursor app session"), - ReaderStatus(providerKey: "xai", label: "Grok", source: "Grok CLI credentials"), + ReaderStatus(providerKey: "grok", label: "Grok", source: "Grok CLI credentials"), ReaderStatus(providerKey: "grok-bot", label: "Grok Bot", source: "Cursor app session"), ReaderStatus(providerKey: "minimax", label: "MiniMax", source: "MiniMax CLI credentials"), ] @@ -1065,7 +1065,8 @@ struct SettingsLogoStylePage: View { case "codex", "openai": return "Codex" case "gemini": return "Gemini" case "antigravity", "google-antigravity": return "Antigravity" - case "grok", "xai": return "Grok" + case "grok": return "Grok" + case "xai", "x-ai": return "xAI" case "grok-bot": return "Grok Bot" case "minimax": return "MiniMax" default: return rowId.capitalized