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
14 changes: 12 additions & 2 deletions Sources/CodexBar/MenuBarLayoutEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,12 @@ struct MenuBarLayoutPreview: View {
snapshot: snapshot)
session = semanticWindows.session
weekly = semanticWindows.weekly
// Provider-specific by design: Mistral's automatic lane can explicitly select its Monthly Plan window.
let automaticPreference = provider == .mistral
? self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot)
: .automatic
rawAutomatic = MenuBarMetricWindowResolver.rateWindow(
preference: .automatic,
preference: automaticPreference,
provider: provider,
snapshot: snapshot,
supportsAverage: self.settings.menuBarMetricSupportsAverage(for: provider),
Expand All @@ -717,6 +721,7 @@ struct MenuBarLayoutPreview: View {
.flatMap { UsagePaceText.weeklyDetail(provider: provider, pace: $0, now: now).rightLabel }
let cost = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot
let costToday = MenuBarLayoutCostResolver.todayCostUSD(snapshot: cost, now: now)
let automaticRenderWindow = MenuBarLayoutRenderWindow(automatic)
return MenuBarLayoutRenderData(
provider: provider,
iconKey: provider.rawValue,
Expand All @@ -726,7 +731,11 @@ struct MenuBarLayoutPreview: View {
weekly: MenuBarLayoutRenderWindow(weekly),
scopedWeekly: MenuBarLayoutRenderWindow(scopedNamed?.window),
scopedWeeklyTitle: scopedNamed?.title,
automatic: MenuBarLayoutRenderWindow(automatic),
automatic: automaticRenderWindow,
// Provider-specific by design: Mistral uses spend text when its automatic lane has no percentage window.
automaticText: provider == .mistral && automaticRenderWindow == nil
? StatusItemController.mistralSpendDisplayText(snapshot: snapshot)
: nil,
sessionPace: self.store.menuBarLayoutPaceText(
provider: provider,
window: session,
Expand Down Expand Up @@ -782,6 +791,7 @@ struct MenuBarLayoutPreview: View {
scopedWeekly: MenuBarLayoutRenderWindow(scopedWeekly),
scopedWeeklyTitle: "Fable only",
automatic: MenuBarLayoutRenderWindow(session),
automaticText: nil,
sessionPace: samplePace(session),
weeklyPace: samplePace(weekly),
automaticPace: samplePace(session),
Expand Down
34 changes: 28 additions & 6 deletions Sources/CodexBar/MenuBarLayoutRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct MenuBarLayoutRenderData: Hashable {
/// `.scopedWeekly` token with the real model rather than assuming Fable.
let scopedWeeklyTitle: String?
let automatic: MenuBarLayoutRenderWindow?
/// Provider-specific text used by the automatic percent token when no percentage window exists.
let automaticText: String?
/// Signed pace deltas per window, already formatted (`+11%`, `-8%`, `0%`). Pace needs the store's
/// historical dataset and work-day setting, so it is resolved upstream like `runsOut` rather than
/// derived from the render windows here.
Expand Down Expand Up @@ -334,8 +336,11 @@ final class MenuBarLayoutRenderer {
attributes: style.attributes)
case let .percent(window):
let rateWindow = Self.window(window, data: data)
let percent = rateWindow.map { options.showUsed ? $0.usedPercent : $0.remainingPercent }
let value = percent.map(UsageFormatter.percentString) ?? Self.missingValue
let resolvedValue = Self.percentValue(
window: window,
rateWindow: rateWindow,
automaticText: data.automaticText,
showUsed: options.showUsed)
let prefix: String
let accessibilityPrefix: String
switch window {
Expand All @@ -353,10 +358,10 @@ final class MenuBarLayoutRenderer {
prefix = ""
accessibilityPrefix = L("Usage")
}
let display = prefix.isEmpty ? value : "\(prefix) \(value)"
let accessibility = percent == nil
? L("%@ unavailable", accessibilityPrefix)
: L("%@ %@", accessibilityPrefix, value)
let display = prefix.isEmpty ? resolvedValue.text : "\(prefix) \(resolvedValue.text)"
let accessibility = resolvedValue.isAvailable
? L("%@ %@", accessibilityPrefix, resolvedValue.text)
: L("%@ unavailable", accessibilityPrefix)
return self.textToken(display, accessibilityText: accessibility, attributes: style.attributes)
case let .pace(window):
let accessibilityPrefix = Self.paceAccessibilityPrefix(window, data: data)
Expand Down Expand Up @@ -424,6 +429,23 @@ final class MenuBarLayoutRenderer {
L("%@ icon", data.providerName ?? L("Provider"))
}

private static func percentValue(
window: PercentWindow,
rateWindow: MenuBarLayoutRenderWindow?,
automaticText: String?,
showUsed: Bool)
-> (text: String, isAvailable: Bool)
{
if let rateWindow {
let percent = showUsed ? rateWindow.usedPercent : rateWindow.remainingPercent
return (UsageFormatter.percentString(percent), true)
}
if window == .automatic, let automaticText {
return (automaticText, true)
}
return (Self.missingValue, false)
}

private static func compactRunsOutText(_ text: String) -> String {
let nowLabel = L("Runs out now")
if text.hasPrefix(nowLabel) {
Expand Down
13 changes: 11 additions & 2 deletions Sources/CodexBar/StatusItemController+MenuBarLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ extension StatusItemController {
let costStrings = self.menuBarLayoutCostStrings(provider: provider, now: now)
let providerName = L(self.store.metadata(for: provider).displayName)
let accountLabel = self.menuBarLayoutAccountLabel(provider: provider, snapshot: snapshot)
let automatic = MenuBarLayoutRenderWindow(windows.automatic)

return MenuBarLayoutRenderData(
provider: provider,
Expand All @@ -85,7 +86,11 @@ extension StatusItemController {
weekly: MenuBarLayoutRenderWindow(windows.weekly),
scopedWeekly: MenuBarLayoutRenderWindow(scopedNamed?.window),
scopedWeeklyTitle: scopedNamed?.title,
automatic: MenuBarLayoutRenderWindow(windows.automatic),
automatic: automatic,
// Provider-specific by design: Mistral uses spend text when its automatic lane has no percentage window.
automaticText: provider == .mistral && automatic == nil
? Self.mistralSpendDisplayText(snapshot: snapshot)
: nil,
Comment thread
kiranmagic7 marked this conversation as resolved.
sessionPace: self.store.menuBarLayoutPaceText(
provider: provider,
window: windows.session,
Expand Down Expand Up @@ -159,8 +164,12 @@ extension StatusItemController {
let semanticWindows = MenuBarLayoutSemanticWindowResolver.windows(
provider: provider,
snapshot: snapshot)
// Provider-specific by design: Mistral's automatic lane can explicitly select its Monthly Plan window.
let automaticPreference = provider == .mistral
? self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot)
: .automatic
let automatic = MenuBarMetricWindowResolver.rateWindow(
preference: .automatic,
preference: automaticPreference,
provider: provider,
snapshot: snapshot,
supportsAverage: self.settings.menuBarMetricSupportsAverage(for: provider),
Expand Down
4 changes: 4 additions & 0 deletions Tests/CodexBarTests/MenuBarLayoutRendererTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ struct MenuBarLayoutRendererTests {
scopedWeekly: nil,
scopedWeeklyTitle: nil,
automatic: nil,
automaticText: nil,
sessionPace: nil,
weeklyPace: nil,
automaticPace: nil,
Expand Down Expand Up @@ -259,6 +260,7 @@ struct MenuBarLayoutRendererTests {
scopedWeekly: nil,
scopedWeeklyTitle: nil,
automatic: nil,
automaticText: nil,
// Pace is suppressed below 3% of window elapsed; the percent token must survive that.
sessionPace: nil,
weeklyPace: nil,
Expand Down Expand Up @@ -472,6 +474,7 @@ struct MenuBarLayoutRendererTests {
scopedWeekly: nil,
scopedWeeklyTitle: nil,
automatic: textOnlyWindow,
automaticText: nil,
sessionPace: nil,
weeklyPace: nil,
automaticPace: nil,
Expand Down Expand Up @@ -585,6 +588,7 @@ struct MenuBarLayoutRendererTests {
windowMinutes: 300,
resetsAt: automaticResetAt ?? self.now.addingTimeInterval(2 * 60 * 60),
resetDescription: nil)),
automaticText: nil,
sessionPace: "-8%",
weeklyPace: "+11%",
automaticPace: "0%",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2498,7 +2498,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+MenuBarLayout.swift",
line: 146,
line: 151,
anchor: "if provider == .codex,",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
Expand Down
123 changes: 123 additions & 0 deletions Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -940,3 +940,126 @@ struct StatusItemBalanceDisplayTests {
updatedAt: Date()).toUsageSnapshot()
}
}

extension StatusItemBalanceDisplayTests {
@Test
func `stored Mistral icon and percent layout preserves selected monthly plan`() {
let settings = self.makeSettings(
suiteName: "StatusItemBalanceDisplayTests-mistral-custom-layout-monthly-plan",
provider: .mistral)
settings.setMenuBarMetricPreference(.monthlyPlan, for: .mistral)
let layout = MenuBarLayout(lines: [[.icon, .percent(window: .automatic)]])
settings.setMenuBarLayout(layout, for: nil)
let (store, controller) = self.makeStoreAndController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let snapshot = MistralUsageSnapshot(
totalCost: 1.2345,
currency: "EUR",
currencySymbol: "€",
totalInputTokens: 10000,
totalOutputTokens: 5000,
totalCachedTokens: 0,
modelCount: 2,
startDate: nil,
endDate: nil,
updatedAt: Date())
.toUsageSnapshot()
.with(extraRateWindows: [
NamedRateWindow(
id: "mistral-monthly-plan",
title: "Monthly Plan",
window: RateWindow(
usedPercent: 42,
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil)),
])

store._setSnapshotForTesting(snapshot, provider: .mistral)
store._setErrorForTesting(nil, provider: .mistral)

let statusItemData = controller.menuBarLayoutRenderData(
provider: .mistral,
snapshot: snapshot,
warningFlash: false)
let previewData = MenuBarLayoutPreview(
layout: layout,
provider: .mistral,
settings: settings,
store: store)
.liveData(provider: .mistral, snapshot: snapshot)

for data in [statusItemData, previewData] {
let rendered = MenuBarLayoutRenderer().render(
layout: layout,
data: data,
icon: NSImage(size: NSSize(width: 16, height: 16)),
options: MenuBarLayoutRenderOptions(
size: .regular,
highContrast: false,
showUsed: true,
appearanceName: "aqua",
isDebugApp: false,
now: Date()))

#expect(data.automatic?.usedPercent == 42)
#expect(data.automaticText == nil)
#expect(rendered.attributedTitle.string.hasSuffix("42%"))
#expect(rendered.accessibilityLabel.contains("42%"))
}
}

@Test
func `stored Mistral icon and percent layout uses api spend in status item and preview`() {
let settings = self.makeSettings(
suiteName: "StatusItemBalanceDisplayTests-mistral-custom-layout",
provider: .mistral)
let layout = MenuBarLayout(lines: [[.icon, .percent(window: .automatic)]])
settings.setMenuBarLayout(layout, for: nil)
let (store, controller) = self.makeStoreAndController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let snapshot = MistralUsageSnapshot(
totalCost: 1.2345,
currency: "EUR",
currencySymbol: "€",
totalInputTokens: 10000,
totalOutputTokens: 5000,
totalCachedTokens: 0,
modelCount: 2,
startDate: nil,
endDate: nil,
updatedAt: Date()).toUsageSnapshot()

store._setSnapshotForTesting(snapshot, provider: .mistral)
store._setErrorForTesting(nil, provider: .mistral)

let statusItemData = controller.menuBarLayoutRenderData(
provider: .mistral,
snapshot: snapshot,
warningFlash: false)
let previewData = MenuBarLayoutPreview(
layout: layout,
provider: .mistral,
settings: settings,
store: store)
.liveData(provider: .mistral, snapshot: snapshot)

for data in [statusItemData, previewData] {
let rendered = MenuBarLayoutRenderer().render(
layout: layout,
data: data,
icon: NSImage(size: NSSize(width: 16, height: 16)),
options: MenuBarLayoutRenderOptions(
size: .regular,
highContrast: false,
showUsed: true,
appearanceName: "aqua",
isDebugApp: false,
now: Date()))

#expect(data.automatic == nil)
#expect(rendered.attributedTitle.string.hasSuffix("€1.2345"))
#expect(rendered.accessibilityLabel.contains("€1.2345"))
}
}
}