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
9 changes: 1 addition & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.48.2 — Unreleased

### Fixed
- Menu: let compact metric detail and reset rows wrap to a second line instead of truncating, so non-English locales keep the full pace and reset information (refs #2182). Thanks @Yuxin-Qiao!
- Kimi: use official usage lane names and hide the Code 7-day row only when it duplicates the primary seven-day quota (matching percentage and reset) (#2741). Thanks @Yuxin-Qiao!
- Menu bar: keep custom reset-countdown tokens aligned with the opened menu by using the exact display clock instead of rounding to the wall minute (#2735). Thanks @hyuntaedotkim!
- Codex: apply a manual reset even when the provider omits the redeemed credit from the next inventory — both samples must corroborate consumption, and the exemption stays one-shot (#2728). Thanks @endless7!
Expand All @@ -29,14 +30,6 @@
### Fixed
- CLI: answer expired serve-cache requests from the last-good response while rebuilding in the background instead of blocking the dashboard for up to a minute.

### Fixed

### Fixed
- Kimi: hide the Code 7-day window when it reports the same percentage and reset time as the primary weekly quota, so the menu no longer shows one allowance twice.

### Fixed
- Menu bar: keep custom reset countdowns aligned with the opened menu instead of rounding their clock back to the start of the wall minute.

## 0.48.0 — 2026-08-06

### Added
Expand Down
11 changes: 9 additions & 2 deletions Sources/CodexBar/MenuCardHeightFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,15 @@ extension UsageMenuCardView.Model.Metric {
return MenuCardHeightFingerprint.join([
self.id,
self.statusText == nil ? "status=0" : "status=1",
presentation.resetText == nil ? "reset=0" : "reset=1",
presentation.metaText == nil ? "meta=0" : "meta=1",
// The title shares horizontal space with a wrapping reset label. Track it only
// when that label exists so ordinary percentage ticks keep the fixed-row cache.
presentation.resetText == nil
? "title=fixed"
: MenuCardHeightFingerprint.field("title", presentation.titleText),
// Reset and meta rows may wrap to a second line, so their text
// content (not just presence) can change the measured card height.
MenuCardHeightFingerprint.field("reset", presentation.resetText),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the title width in the wrapping-row fingerprint

When a stable reset label sits near the wrapping threshold, a width-changing title update—such as 9% left becoming 100% left, or a longer dynamic window title—reduces the space offered to the reset text in the same HStack and can change it from one line to two. This fingerprint still hashes only reset/meta content and intentionally ignores percentage changes, so the old one-line cached height is reused and the new second line can be clipped. Fresh evidence in this revision is that reset wrapping was introduced while the cache key still omits presentation.titleText; include that width-affecting text in the fingerprint as well.

Useful? React with 👍 / 👎.

MenuCardHeightFingerprint.field("meta", presentation.metaText),
self.detailText == nil ? "detail=0" : "detail=1",
self.pacePercent == nil ? "pace=0" : "pace=1",
self.paceOnTop ? "paceTop=1" : "paceTop=0",
Expand Down
7 changes: 5 additions & 2 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,9 @@ private struct MetricRow: View {
Text(resetText)
.font(.footnote)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.lineLimit(2)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
}
}
UsageProgressBar(
Expand All @@ -554,8 +556,9 @@ private struct MetricRow: View {
Text(metaText)
.font(.footnote)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.lineLimit(2)
.truncationMode(.tail)
.fixedSize(horizontal: false, vertical: true)
Comment on lines +559 to +561

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include wrapped metric text in the height-cache fingerprint

When an already-populated metric changes from a short countdown or pace string to a longer one—for example, when a forecast is appended after refresh—these lines now change the card's height. However, Metric.heightFingerprint in MenuCardHeightFingerprint.swift records only whether reset/meta text is present, so makeMenuCardItem can reuse the old measured frame for the same scope and width; the newly added second line is then clipped, defeating this fix until another fingerprint field changes. Include the reset/meta text shape or content in the metric height fingerprint now that these fields can wrap.

Useful? React with 👍 / 👎.

}
if let detail = self.metric.detailText {
Text(detail)
Expand Down
30 changes: 27 additions & 3 deletions Tests/CodexBarTests/MenuCardHeightFingerprintTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct MenuCardHeightFingerprintTests {
}

@Test
func `height fingerprint tracks optional metric rows rather than their text`() {
func `height fingerprint tracks wrapping metric text content`() {
let bare = Self.model(statusText: nil).heightFingerprint(section: "card")
let withReset = Self.model(statusText: nil, resetText: "Resets in 2h").heightFingerprint(section: "card")
let withMeta = Self.model(statusText: nil, detailLeftText: "20% in reserve")
Expand All @@ -52,8 +52,32 @@ struct MenuCardHeightFingerprintTests {

#expect(bare != withReset)
#expect(bare != withMeta)
#expect(withMeta == withChangedMeta)
#expect(withMeta == withFoldedForecast)
// Meta and reset rows can wrap to a second line, so any text change
// must invalidate the cached height instead of reusing a short frame.
#expect(withMeta != withChangedMeta)
#expect(withMeta != withFoldedForecast)
}

@Test
func `height fingerprint invalidates when wrapping text grows from short to long`() {
let shortReset = Self.model(statusText: nil, resetText: "Resets in 2h")
.heightFingerprint(section: "card")
let widerTitle = Self.model(percent: 100, statusText: nil, resetText: "Resets in 2h")
.heightFingerprint(section: "card")
let longReset = Self.model(
statusText: nil,
resetText: "Resets tomorrow at 6:00 AM after a very long localized description")
.heightFingerprint(section: "card")
let shortMeta = Self.model(statusText: nil, detailLeftText: "On pace")
.heightFingerprint(section: "card")
let longMeta = Self.model(
statusText: nil,
detailLeftText: "Estimated 2 session quotas left with a generously long localized pace explanation")
.heightFingerprint(section: "card")

#expect(shortReset != widerTitle)
#expect(shortReset != longReset)
#expect(shortMeta != longMeta)
}

@Test
Expand Down
22 changes: 11 additions & 11 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1942,47 +1942,47 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 635,
line: 638,
anchor: "guard self.model.provider == .doubao else { return nil }",
expectedProviderIDs: ["doubao"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["doubao@0"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1062,
line: 1065,
anchor: "if provider == .kiro,",
expectedProviderIDs: ["kilo", "kiro"],
expectedReferenceCount: 2,
expectedReferenceFingerprint: ["kiro@0", "kilo@5"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1085,
line: 1088,
anchor: "if provider == .minimax {",
expectedProviderIDs: ["codex", "minimax"],
expectedReferenceCount: 2,
expectedReferenceFingerprint: ["minimax@0", "codex@3"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1120,
line: 1123,
anchor: "guard let loginMethod = snapshot?.loginMethod(for: .kilo) else {",
expectedProviderIDs: ["kilo"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["kilo@0"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1204,
line: 1207,
anchor: "if input.provider == .antigravity {",
expectedProviderIDs: ["antigravity", "mistral"],
expectedReferenceCount: 2,
expectedReferenceFingerprint: ["antigravity@0", "mistral@6"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1224,
line: 1227,
anchor: "if input.provider == .codex, let codexProjection = input.codexProjection {",
expectedProviderIDs: ["alibaba", "alibabatokenplan", "codex", "perplexity", "sub2api"],
expectedReferenceCount: 6,
Expand All @@ -1997,31 +1997,31 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1275,
line: 1278,
anchor: "if input.provider == .kilo || input.provider == .kimi,",
expectedProviderIDs: ["kilo", "kimi"],
expectedReferenceCount: 2,
expectedReferenceFingerprint: ["kilo@0", "kimi@0"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1357,
line: 1360,
anchor: "var paceDetail = if input.provider == .kimi {",
expectedProviderIDs: ["kimi"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["kimi@0"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1373,
line: 1376,
anchor: "if input.provider == .warp,",
expectedProviderIDs: ["chutes", "kilo", "kiro", "litellm", "sub2api", "warp"],
expectedReferenceCount: 6,
expectedReferenceFingerprint: ["warp@0", "chutes@7", "kilo@7", "litellm@7", "sub2api@16", "kiro@19"],
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1406,
line: 1409,
anchor: "if input.provider == .alibaba || input.provider == .alibabatokenplan,",
expectedProviderIDs: ["alibaba", "alibabatokenplan", "copilot", "crof", "manus", "perplexity", "zenmux"],
expectedReferenceCount: 8,
Expand All @@ -2038,7 +2038,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."),
AllowedProviderConstruct(
path: "Sources/CodexBar/MenuCardView.swift",
line: 1447,
line: 1450,
anchor: "if input.provider == .synthetic,",
expectedProviderIDs: ["synthetic"],
expectedReferenceCount: 1,
Expand Down
36 changes: 34 additions & 2 deletions Tests/CodexBarTests/UsageMenuCardLayoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ struct UsageMenuCardLayoutTests {
}

@Test
func `metric detail remains one row as pace content grows`() {
func `metric detail wraps to a second row instead of truncating as pace content grows`() {
let width: CGFloat = 296
func card(
detailRightText: String,
Expand Down Expand Up @@ -178,7 +178,39 @@ struct UsageMenuCardLayoutTests {
accessibilityLabel: "Est. 2 session quotas left · 6 windows until reset")))
.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)).height

#expect(abs(shortHeight - longHeight) < Self.heightTolerance)
// The meta line may wrap to two lines so tail content stays readable
// instead of truncating; the card grows by roughly one text line.
#expect(longHeight - shortHeight > Self.heightTolerance)
#expect(longHeight - shortHeight < 20)
}

@Test
func `metric reset wraps to a bounded second line at standard width`() {
let width: CGFloat = 296
func card(resetText: String) -> UsageMenuCardView {
UsageMenuCardView(model: Self.model(metrics: [
UsageMenuCardView.Model.Metric(
id: "weekly",
title: "Weekly",
percent: 69,
percentStyle: .left,
resetText: resetText,
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true),
]), width: width)
}

let shortHeight = NSHostingController(rootView: card(resetText: "Resets in 2h"))
.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)).height
let longHeight = NSHostingController(rootView: card(
resetText: "Resets Wednesday, August 14 at 11:59 PM"))
.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)).height

#expect(longHeight - shortHeight > Self.heightTolerance)
#expect(longHeight - shortHeight < 20)
}

private static func model(
Expand Down