Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- **OpenCode's session-level fallback now bills reasoning tokens at the output rate.** When none of a session's messages produce a priced call, codeburn falls back to the totals on OpenCode's `session` row, and that path handed `tokens_output` alone to the pricing table while still reporting `tokens_reasoning` separately, so a reasoning model's thinking showed up in the token breakdown but was never charged (4,000 reasoning tokens on Sonnet 4 left a $0.11 session priced at $0.05). OpenCode itself charges reasoning at the output rate and codeburn's per-message path already did; the fallback and the per-message path now both go through `billableOutputTokens`, the same rule the cache-rehydration path uses, so the three can't drift apart again. Displayed token totals are unchanged. (#1334)

### Fixed (menubar)
- **The localization catalog test can now see argument-order drift, and every multi-argument key spells its slots positionally.** The catalog comparison matched specifier lists, so a key whose placeholders are all `%@` passed even when the Chinese value bound the arguments into the wrong slots — exactly the drift that shipped in three early-reset keys and was only caught by rendering real data bilingually. All 42 multi-argument keys in both tables now use positional specifiers (`%1$@`, `%2$lld`, …), the parity check compares slot to verb so Chinese word order stays free while each slot keeps its type, and a new test fails on any multi-argument key that is not positional. The source scanner also stopped being blind to three shapes that put copy on screen looking like machinery: a ternary (`Text(flag ? "A" : "B")`), a concatenation (`.help(prefix + " suffix")`), and a literal bound to a `let` and passed by name — `Text(verbatim:)` stays the explicit opt-out, and the four provider-name fallbacks the new rules surfaced in HeatmapSection now route through the catalog as verbatim entries. (#1331)
- **A quota window that resets now refreshes now.** The menubar's 30s tick compared the reset times it already holds — Claude's 5-hour and weekly windows, Codex's two windows, every Capacity Dock provider's — against the previous tick, and forces a refresh for the provider whose window just rolled over instead of leaving the pre-reset percentage on screen for up to five minutes. Each reset instant fires once; everything else keeps the existing cadence.
- **The popover's quota warning can be read in light mode, and says which limit it is warning about.** The banner under the tagline painted its text in the same system yellow, orange or red as its 12% pill, so in light mode warning text measured 1.2:1 to 1.4:1 against the pill, and even red reached only 3.0:1; in dark mode red fell to 3.3:1. The pill keeps its hue, and the text and its glyph now take a per-appearance colour for each severity, a deep amber, rust or brick in light mode and a lifted orange or red in dark mode, that measures 5.3:1 or better everywhere. A test holds every severity at WCAG AA (4.5:1) against the pill composited over both ends of the popover's background in both appearances. The sentence also named no window. Its figure is the provider's worst window, so "Claude 70% of quota used" meant the 5-hour limit while Claude's own panel read weekly 34%, and it looked wrong. It now reads `Claude · 5-hour 71% · resets in 3h 12m`, with the countdown worded exactly as the Capacity Dock words it and one line per provider when several are warning. The countdown is left off when the provider reports no reset time or the reset has already passed. "Over limit" is kept for a window at or past 100%, where it used to cover anything from 90%. The flame's colour and the providers that trigger the banner are unchanged.

Expand Down
8 changes: 4 additions & 4 deletions mac/Sources/CodeBurnMenubar/CodeBurnApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
let menubarPeriod = store.menubarPeriod
if let shortfall = store.menubarBadgeDeviceShortfall {
button.toolTip = L(
"CodeBurn %@ · %lld of %lld devices reporting",
"CodeBurn %1$@ · %2$lld of %3$lld devices reporting",
menubarPeriod.menubarMetricLabel,
shortfall.reachable,
shortfall.total
Expand Down Expand Up @@ -1740,7 +1740,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
private func contextMenuUsageSummary() -> String {
guard let current = store.todayPayload?.current else { return L("Today · no usage yet") }
let calls = current.calls == 1 ? L("1 call") : L("%lld calls", current.calls)
return L("Today · %@ · %@", current.cost.asCurrency(), calls)
return L("Today · %1$@ · %2$@", current.cost.asCurrency(), calls)
}

private var settingsWindowController: NSWindowController?
Expand Down Expand Up @@ -1821,13 +1821,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM
} else if updateChecker.updateAvailable, let latest = updateChecker.latestVersion {
alert.messageText = L("Update Available")
let header = L(
"%@ is available (you have %@).",
"%1$@ is available (you have %2$@).",
AppVersion.display(latest),
AppVersion.display(updateChecker.currentVersion)
)
if updateChecker.cliTooOldForUpdate {
alert.informativeText = L(
"%@ Your codeburn CLI is too old to install it. First run:\n\n%@\n\nthen:\n\ncodeburn menubar --force",
"%1$@ Your codeburn CLI is too old to install it. First run:\n\n%2$@\n\nthen:\n\ncodeburn menubar --force",
header,
updateChecker.cliUpdateCommand
)
Expand Down
8 changes: 4 additions & 4 deletions mac/Sources/CodeBurnMenubar/Data/CodexBankedResets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ enum CodexBankedResetPresentation {
if let grant = credits.latestGrant, let grantedAt = grant.grantedAt {
let type = resetTypeLabel(grant.resetType)
parts.append(grantedAt > now
? L("next %@ lands %@", type, compactAge(of: grantedAt, now: now))
: L("latest %@ granted %@", type, compactAge(of: grantedAt, now: now)))
? L("next %1$@ lands %2$@", type, compactAge(of: grantedAt, now: now))
: L("latest %1$@ granted %2$@", type, compactAge(of: grantedAt, now: now)))
}
if let expiry = credits.nextExpiresAt {
parts.append(L("next expires %@", compactAge(of: expiry, now: now)))
Expand All @@ -147,8 +147,8 @@ enum CodexBankedResetPresentation {
var body: String
if let grantedAt = grant.grantedAt {
body = grantedAt > now
? L("A %@ lands %@.", type, compactAge(of: grantedAt, now: now))
: L("A %@ was added to your account %@.", type, compactAge(of: grantedAt, now: now))
? L("A %1$@ lands %2$@.", type, compactAge(of: grantedAt, now: now))
: L("A %1$@ was added to your account %2$@.", type, compactAge(of: grantedAt, now: now))
} else {
body = L("A %@ was added to your account.", type)
}
Expand Down
2 changes: 1 addition & 1 deletion mac/Sources/CodeBurnMenubar/Data/CodexUsage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ struct CodexUsage: Sendable, Equatable {
}
// The two figures are already grouped by the formatter above, so
// they are substituted formatted and only the sentence is translated.
let base = L("Monthly usage limit · %@ / %@ credits", text(used), text(limit))
let base = L("Monthly usage limit · %1$@ / %2$@ credits", text(used), text(limit))
return reached ? L("%@ · limit reached", base) : base
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ enum CopilotQuotaPresentation {
// The host is an API hostname and the plan comes from GitHub; both are
// substituted verbatim, only the sentence around them is translated.
guard let plan, !plan.isEmpty else { return L("Live quota tracked from %@.", host) }
return L("Plan: %@. Live quota tracked from %@.", plan, host)
return L("Plan: %1$@. Live quota tracked from %2$@.", plan, host)
}

static func settingsNotConnectedDetail(explicitlyDisconnected: Bool) -> String {
Expand Down
20 changes: 10 additions & 10 deletions mac/Sources/CodeBurnMenubar/Data/EarlyQuotaReset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ struct EarlyQuotaResetEvent: Codable, Equatable, Sendable {
switch signal {
case .resetMovedForward:
return L(
"%@'s %@ reset %@ early. %@",
"%1$@'s %2$@ reset %3$@ early. %4$@",
providerName, EarlyQuotaResetFormat.limitName(windowName), lead, back
)
// The reset time did not move: the vendor emptied the counter inside the
// cycle, which still ends when it always would have. Saying "reset early"
// here would promise a whole new window that is not coming.
case .usageDropped:
return L(
"%@ cleared your %@ %@ before its reset. %@",
"%1$@ cleared your %2$@ %3$@ before its reset. %4$@",
providerName, EarlyQuotaResetFormat.usageName(windowName), lead, back
)
}
Expand All @@ -92,13 +92,13 @@ struct EarlyQuotaResetEvent: Codable, Equatable, Sendable {
switch signal {
case .resetMovedForward:
return L(
"%@ reset %@ early",
"%1$@ reset %2$@ early",
EarlyQuotaResetFormat.capitalizedFirst(EarlyQuotaResetFormat.limitName(windowName)),
lead
)
case .usageDropped:
let usage = EarlyQuotaResetFormat.usageName(windowName)
return L("%@ cleared, %@ before reset", EarlyQuotaResetFormat.capitalizedFirst(usage), lead)
return L("%1$@ cleared, %2$@ before reset", EarlyQuotaResetFormat.capitalizedFirst(usage), lead)
}
}

Expand All @@ -111,12 +111,12 @@ struct EarlyQuotaResetEvent: Codable, Equatable, Sendable {
switch signal {
case .resetMovedForward:
return L(
"%@ reset this %@ %@ before its scheduled time. %@",
"%1$@ reset this %2$@ %3$@ before its scheduled time. %4$@",
providerName, EarlyQuotaResetFormat.limitName(windowName), lead, available
)
case .usageDropped:
return L(
"%@ cleared this %@ %@ before the window's scheduled reset, which has not moved. %@",
"%1$@ cleared this %2$@ %3$@ before the window's scheduled reset, which has not moved. %4$@",
providerName, EarlyQuotaResetFormat.usageName(windowName), lead, available
)
}
Expand Down Expand Up @@ -271,13 +271,13 @@ enum EarlyQuotaResetHistory {
let noun = EarlyQuotaResetFormat.windowNoun(windowName)
let lead = EarlyQuotaResetFormat.approximateLead(seconds: typicalEarlyBySeconds)
if earlyResets == 1 && observedResets == 1 {
return L("Last %@ reset came ~%@ early", noun, lead)
return L("Last %1$@ reset came ~%2$@ early", noun, lead)
}
if earlyResets == observedResets {
return L("Last %lld %@ resets came ~%@ early", earlyResets, noun, lead)
return L("Last %1$lld %2$@ resets came ~%3$@ early", earlyResets, noun, lead)
}
return L(
"%lld of the last %lld %@ resets came ~%@ early",
"%1$lld of the last %2$lld %3$@ resets came ~%4$@ early",
earlyResets, observedResets, noun, lead
)
}
Expand Down Expand Up @@ -370,7 +370,7 @@ enum EarlyQuotaResetFormat {
let hours = Int((seconds / 3600).rounded())
guard hours >= 24 else { return L("%lldh", hours) }
let rest = hours % 24
return rest == 0 ? L("%lldd", hours / 24) : L("%lldd %lldh", hours / 24, rest)
return rest == 0 ? L("%lldd", hours / 24) : L("%1$lldd %2$lldh", hours / 24, rest)
}

/// "18h", "2d" — rounded, for a pattern that is only ever approximate.
Expand Down
4 changes: 2 additions & 2 deletions mac/Sources/CodeBurnMenubar/Data/QuotaCrossing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ struct QuotaCrossingEvent: Equatable, Sendable {

var notificationTitle: String {
switch level {
case .warning: L("%@ · %@ at 80%%", providerName, windowLabel)
case .limit: L("%@ · %@ limit reached", providerName, windowLabel)
case .warning: L("%1$@ · %2$@ at 80%%", providerName, windowLabel)
case .limit: L("%1$@ · %2$@ limit reached", providerName, windowLabel)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ enum QuotaPacePresentation {
let hours = minutes / 60
let days = hours / 24
// Same three keys the popover's reset countdown uses.
if days > 0 { return L("%lldd %lldh", days, hours % 24) }
if hours > 0 { return L("%lldh %lldm", hours, minutes % 60) }
if days > 0 { return L("%1$lldd %2$lldh", days, hours % 24) }
if hours > 0 { return L("%1$lldh %2$lldm", hours, minutes % 60) }
return L("%lldm", minutes)
}
}
4 changes: 2 additions & 2 deletions mac/Sources/CodeBurnMenubar/Data/QuotaSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ extension QuotaSummary.Window {
let hours = minutes / 60
let days = hours / 24
// d/h/m are unit abbreviations; zh-Hans uses 天/小时/分.
if days > 0 { return L("%lldd %lldh", days, hours % 24) }
if hours > 0 { return L("%lldh %lldm", hours, minutes % 60) }
if days > 0 { return L("%1$lldd %2$lldh", days, hours % 24) }
if hours > 0 { return L("%1$lldh %2$lldm", hours, minutes % 60) }
return L("%lldm", minutes)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ enum QuotaWarningPresentation {
// "Over limit" only once the window really is. Severity turns `.danger`
// at 90%, and "over limit (93%)" is not true.
var text = warning.percent >= 100
? L("%@ over limit (%lld%%)", subject, percent)
? L("%1$@ over limit (%2$lld%%)", subject, percent)
: "\(subject) \(percent)%"
if let countdown = resetCountdown(warning.resetsAt, now: now) {
text += " · " + L("resets in %@", countdown)
Expand Down
4 changes: 2 additions & 2 deletions mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ final class UpdateChecker {
isUpdating = false
updateFailureStage = .cliUpdate
updateError = L(
"Could not find the package manager for %@. Run “%@” manually, then try again.",
"Could not find the package manager for %1$@. Run “%2$@” manually, then try again.",
cliPath.isEmpty ? L("the CLI") : cliPath,
cliUpdateCommand
)
Expand Down Expand Up @@ -362,7 +362,7 @@ final class UpdateChecker {
if cliTooOldForUpdate {
updateFailureStage = .menubarUpdate
updateError = L(
"Your codeburn CLI (%@) is too old to update the menubar. Run “%@” first, then try again.",
"Your codeburn CLI (%1$@) is too old to update the menubar. Run “%2$@” first, then try again.",
AppVersion.display(installedCliVersion ?? ""),
cliUpdateCommand
)
Expand Down
4 changes: 2 additions & 2 deletions mac/Sources/CodeBurnMenubar/MenubarSecondRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ enum MenubarRowFormatter {
let hours = minutes / 60
let days = hours / 24
// d/h/m are unit abbreviations; zh-Hans uses 天/小时/分.
if days > 0 { return L("%lldd %lldh", days, hours % 24) }
if hours > 0 { return L("%lldh %lldm", hours, minutes % 60) }
if days > 0 { return L("%1$lldd %2$lldh", days, hours % 24) }
if hours > 0 { return L("%1$lldh %2$lldm", hours, minutes % 60) }
return L("%lldm", minutes)
}

Expand Down
Loading
Loading