Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions Sources/CodeCaps/GlanceViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? ""
}

Expand Down Expand Up @@ -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)
Expand All @@ -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: ", ")
Expand Down Expand Up @@ -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 {
Expand Down
94 changes: 67 additions & 27 deletions Sources/CodeCaps/PipWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -118,30 +133,37 @@ 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 {
self != .barsOnly && self != .singleBar
}

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
}
}

Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions Sources/CodeCaps/PlatformLogo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -484,7 +485,7 @@ private static func variantFile(base: String, style: MarkStyle, isDark: Bool) ->

private static let monochromeKeys: Set<String> = [
"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`
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions Sources/CodeCaps/Resources/ProviderMarks/xai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions Sources/CodeCaps/SettingsViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/QuotaCore/LocalQuotaReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down
6 changes: 4 additions & 2 deletions Sources/QuotaCore/QuotaModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
jaywedgeworth22 marked this conversation as resolved.
Expand All @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion Sources/QuotaCore/WidgetPlatformDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading