fix(ai-usage): 隐藏无额度来源的菜单栏占位
This commit is contained in:
1 parent
23d015a000
commit
bfce9c6cc5
2 files changed
+14
-42
No files matched your search
@@ -45,8 +45,7 @@ struct MenuBarReading {
|
||||
var batteryHistory: [Double] = []
|
||||
/// 本机日志今日 Token 总量;扫描未开启或还没有数据时为 nil
|
||||
var aiTokens: Int?
|
||||
/// 每个已启用来源各占一个紧凑读数;没有额度时显示占位,避免来源从菜单栏消失。
|
||||
var aiQuotaProviders: [AIProviderID] = []
|
||||
/// 只有可显示额度的来源才占用菜单栏读数;本机 Token 统计独立于订阅额度。
|
||||
var aiQuotas: [MenuBarQuota] = []
|
||||
var batteryCharging = false
|
||||
/// 附在电池项旁的蓝牙设备:电量低的那一个,或没有电池的 Mac 上电量最低的那一个
|
||||
@@ -74,12 +73,10 @@ struct MenuBarReading {
|
||||
battery = store.battery?.level
|
||||
batteryHistory = battery.map { Array(repeating: $0, count: 30) } ?? []
|
||||
aiTokens = model.aiUsage.todayTokens
|
||||
aiQuotaProviders = model.settings.aiUsageEnabled
|
||||
? AIProviderID.allCases.filter { model.settings.aiUsageSources.contains($0) } : []
|
||||
aiQuotas = model.aiUsage.visibleQuotaProviders(for: nil).compactMap { provider in
|
||||
let state = model.aiUsage.visibleQuotaState(for: provider)
|
||||
guard let snapshot = state.snapshot else { return nil }
|
||||
// 已过重置时间的窗口不再代表当前额度(常见于重启后离线恢复的缓存),宁可显示占位。
|
||||
// 已过重置时间的窗口不再代表当前额度(常见于重启后离线恢复的缓存),不显示过期额度。
|
||||
let now = Date()
|
||||
let windows = snapshot.windows.filter { $0.resetsAt.map { $0 > now } ?? true }
|
||||
// 周额度比短时会话额度更适合作为常驻读数;模型专属周额度仅在通用周额度缺席时使用。
|
||||
@@ -151,13 +148,10 @@ struct MenuBarReading {
|
||||
case .battery:
|
||||
battery.map { tr("电池 \(Format.percent($0))") + (batteryCharging ? tr(",充电中") : "") }
|
||||
?? bluetoothDevice.map { tr("蓝牙设备电量 \($0.percent)%") }
|
||||
// 没有读数也要给出一行,否则菜单栏只剩一个不会解释自己的 “AI —”
|
||||
// 没有订阅额度时仍解释本机 Token 读数。
|
||||
case .aiUsage:
|
||||
if !aiQuotas.isEmpty {
|
||||
aiQuotaProviders.map { provider in
|
||||
guard let quota = aiQuotas.first(where: { $0.provider == provider }) else {
|
||||
return "\(provider == .codex ? "Codex" : "Claude") · \(tr("暂无额度数据"))"
|
||||
}
|
||||
aiQuotas.map { quota in
|
||||
let previous = quota.isStale ? "\(tr("上次额度")) · " : ""
|
||||
let checked = quota.isStale
|
||||
? " · \(tr("上次检查:\(quota.fetchedAt.formatted(Date.FormatStyle(date: .abbreviated, time: .shortened, locale: L10n.locale)))"))"
|
||||
@@ -301,31 +295,12 @@ enum MenuBarRenderer {
|
||||
|
||||
private static func aiUsageSegment(reading: MenuBarReading, style: MenuBarStyle) -> Segment {
|
||||
guard !reading.aiQuotas.isEmpty else {
|
||||
if style == .icon, reading.aiQuotaProviders.count == 1,
|
||||
let provider = reading.aiQuotaProviders.first {
|
||||
return combine([aiProviderLogo(provider),
|
||||
inlineValue(reading.aiTokens.map { tokenText($0) } ?? "—",
|
||||
sample: "999.9M", alert: false)])
|
||||
}
|
||||
return textSegment(item: .aiUsage, value: reading.aiTokens.map { tokenText($0) } ?? "—",
|
||||
sample: "999.9M", style: style)
|
||||
}
|
||||
let providers = reading.aiQuotaProviders.count > 1
|
||||
? reading.aiQuotaProviders : [reading.aiQuotas[0].provider]
|
||||
let status = combine(providers.map { provider in
|
||||
let status = combine(reading.aiQuotas.map { quota in
|
||||
let provider = quota.provider
|
||||
let name = provider == .codex ? "Codex" : "Claude"
|
||||
guard let quota = reading.aiQuotas.first(where: { $0.provider == provider }) else {
|
||||
switch style {
|
||||
case .ring, .history, .line, .pie, .meter:
|
||||
return aiQuotaInlineText(label: name, value: "—")
|
||||
case .inline:
|
||||
return inlineText(label: name, value: "—", sample: "100%", alert: false)
|
||||
case .icon:
|
||||
return combine([aiProviderLogo(provider), inlineValue("—", sample: "100%", alert: false)])
|
||||
case .stacked, .dot:
|
||||
return aiQuotaText(label: name, value: "—")
|
||||
}
|
||||
}
|
||||
let label = "\(name) · \(quota.shortWindowName)"
|
||||
let value = "\(quota.remainingPercent)%"
|
||||
let stacked = aiQuotaText(label: name, value: value)
|
||||
|
||||
@@ -100,11 +100,10 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
#expect(codexQuota?.window.kind == .weekly)
|
||||
#expect(codexQuota?.remainingPercent == 27)
|
||||
#expect(codexQuota?.resetText != "—")
|
||||
#expect(reading.aiQuotaProviders == [.codex, .claude])
|
||||
#expect(reading.aiQuotas.map(\.provider) == [.codex, .claude])
|
||||
let both = MenuBarRenderer.image(reading: reading, items: [.aiUsage],
|
||||
style: { _ in .stacked }, networkStyle: .dots, colorizeHighLoad: false, fahrenheit: false)
|
||||
var codexOnly = reading
|
||||
codexOnly.aiQuotaProviders = [.codex]
|
||||
codexOnly.aiQuotas = reading.aiQuotas.filter { $0.provider == .codex }
|
||||
let single = MenuBarRenderer.image(reading: codexOnly, items: [.aiUsage],
|
||||
style: { _ in .stacked }, networkStyle: .dots, colorizeHighLoad: false, fahrenheit: false)
|
||||
@@ -112,7 +111,6 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
let codexIcon = MenuBarRenderer.image(reading: codexOnly, items: [.aiUsage],
|
||||
style: { _ in .icon }, networkStyle: .dots, colorizeHighLoad: false, fahrenheit: false)
|
||||
var claudeOnly = reading
|
||||
claudeOnly.aiQuotaProviders = [.claude]
|
||||
claudeOnly.aiQuotas = reading.aiQuotas.filter { $0.provider == .claude }
|
||||
let claudeIcon = MenuBarRenderer.image(reading: claudeOnly, items: [.aiUsage],
|
||||
style: { _ in .icon }, networkStyle: .dots, colorizeHighLoad: false, fahrenheit: false)
|
||||
@@ -134,7 +132,7 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
#expect(!tooltip.contains("5%"))
|
||||
}
|
||||
|
||||
@Test func menuBarNamesAnEnabledSourceWithoutQuotaBesideTheAvailableOne() async {
|
||||
@Test func menuBarOmitsSourceWithoutQuotaBesideTheAvailableOne() async {
|
||||
let settings = AppSettings(defaults: defaultsForAIUsage())
|
||||
settings.aiUsageEnabled = true
|
||||
let codex = AIQuotaSnapshot(provider: .codex, windows: [
|
||||
@@ -147,10 +145,8 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
await model.aiUsage.refresh()
|
||||
|
||||
let reading = MenuBarReading(model: model)
|
||||
#expect(reading.aiQuotas.count == 1)
|
||||
let claudeLine = reading.tooltip(items: [.aiUsage], fahrenheit: false)
|
||||
.split(separator: "\n").first { $0.hasPrefix("Claude · ") }
|
||||
#expect(claudeLine?.contains("%") == false)
|
||||
#expect(reading.aiQuotas.map(\.provider) == [.codex])
|
||||
#expect(!reading.tooltip(items: [.aiUsage], fahrenheit: false).contains("Claude"))
|
||||
let image = MenuBarRenderer.image(reading: reading, items: [.aiUsage],
|
||||
style: { _ in .stacked }, networkStyle: .dots, colorizeHighLoad: false, fahrenheit: false)
|
||||
#expect(image.size.width > 0)
|
||||
@@ -195,8 +191,7 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
#expect(reading.aiQuotas.first?.window.kind == .session)
|
||||
#expect(reading.aiQuotas.first?.remainingPercent == 70)
|
||||
let tooltip = reading.tooltip(items: [.aiUsage], fahrenheit: false)
|
||||
let claudeLine = tooltip.split(separator: "\n").first { $0.hasPrefix("Claude · ") }
|
||||
#expect(claudeLine?.contains("%") == false, "tooltip: \(tooltip)")
|
||||
#expect(!tooltip.contains("Claude"), "tooltip: \(tooltip)")
|
||||
#expect(!tooltip.contains("5%"), "tooltip: \(tooltip)")
|
||||
}
|
||||
|
||||
@@ -208,7 +203,9 @@ private actor GatedUsageProvider: AIUsageProvider {
|
||||
aiQuotaProviders: [StubQuotaProvider(results: [.failure(.notConfigured)])])
|
||||
await model.aiUsage.refresh()
|
||||
|
||||
let tooltip = MenuBarReading(model: model).tooltip(items: [.aiUsage], fahrenheit: false)
|
||||
let reading = MenuBarReading(model: model)
|
||||
#expect(reading.aiQuotas.isEmpty)
|
||||
let tooltip = reading.tooltip(items: [.aiUsage], fahrenheit: false)
|
||||
#expect(tooltip.contains("42 Tokens"))
|
||||
#expect(!tooltip.contains("剩余"))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user