Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c3a8793
style: apply release formatting
steipete Jun 9, 2026
920997c
docs: update appcast for 0.32.5
steipete Jun 9, 2026
a4f278d
chore: start 0.32.6 development
steipete Jun 9, 2026
f91d779
Add T3 Chat to the README (#1361)
Quicksaver Jun 10, 2026
6f6cb09
Fix Antigravity summaries for untracked quotas (#1369)
Martin-Hausleitner Jun 10, 2026
20004f3
fix: correct Claude usage pricing (#1372)
steipete Jun 10, 2026
e97bfb0
fix: defer tracked menu refresh rebuilds (#1376)
jangisaac-dev Jun 10, 2026
050b139
feat: support more MiMo browsers (#1350)
Yuxin-Qiao Jun 10, 2026
f51db0e
fix: add Claude web session recovery action (#1378)
LeoLin990405 Jun 10, 2026
7c0ed03
perf: reuse hosted chart submenus (#1384)
steipete Jun 10, 2026
1246ec6
Fix: schedule idle prune so cached dashboard WebViews are evicted (#1…
naoterumaker Jun 10, 2026
a317098
Fix Doubao false 100% request usage (#1383)
LeoLin990405 Jun 10, 2026
4c092ce
fix(menu): defer copy-icon click work off the NSMenu tracking loop (m…
LeoLin990405 Jun 10, 2026
2e42427
Add configurable terminal app for Open Terminal (#1225)
Yuxin-Qiao Jun 10, 2026
a6bb677
Fix Copilot unlimited chat quota display (#1320)
soumikbhatta Jun 10, 2026
f561834
Avoid repeated CodexBarCache keychain prompts in dev builds (#1271)
Yuxin-Qiao Jun 10, 2026
2eaa313
Make Augment keepalive stop idempotent (#1262)
ProspectOre Jun 10, 2026
04ac21c
Add Japanese (ja) localization (#1385)
naoterumaker Jun 10, 2026
2ed15bf
perf: defer overview provider rebuild (#1391)
steipete Jun 10, 2026
08c171b
[security] fix(providers): guard credentialed redirects (#1237)
Hinotoi-agent Jun 10, 2026
71a38c2
fix: keep Codex cost visible without quotas (#1390)
vaibhavarora14 Jun 10, 2026
cde92cf
perf: add menu chart render timing logs (#1366)
scheinms Jun 10, 2026
ee063f9
test: stabilize dashboard idle prune scheduling (#1395)
steipete Jun 10, 2026
91305ae
fix: show all daily cost models (#1396)
steipete Jun 10, 2026
f927e8a
Recycle menu card hosting views and reconcile menu content in place (…
bcssewl Jun 10, 2026
3c2d53d
Fix menu open refresh delay (#1398)
joshuavial Jun 10, 2026
eda747b
Gate switcher shortcut event peek behind session HID counters (#1397)
bcssewl Jun 10, 2026
b7772b2
Defer merged icon redraws during menu tracking (#1409)
kiranmagic7 Jun 11, 2026
0e0102c
Fix merged menu width after provider switches (#1411)
steipete Jun 11, 2026
02c9403
Decode provider status feeds off the main actor (#1406)
ProspectOre Jun 11, 2026
ad71da0
Keep codex account file reads off the menu-build path
ProspectOre Jun 10, 2026
f1c1b6c
perf: keep Codex account reconciliation off menu tracking
steipete Jun 11, 2026
febf562
Merge pull request #1401 from ProspectOre/perf/menu-account-snapshot-…
steipete Jun 11, 2026
fda0fae
Run cost-usage corpus scans off the Swift cooperative thread pool
ProspectOre Jun 10, 2026
01350b2
fix: make cost scan cancellation immediate
steipete Jun 11, 2026
e7aa652
test: isolate cost scan executor queues
steipete Jun 11, 2026
4c964cc
Merge pull request #1402 from ProspectOre/perf/cost-scan-off-cooperat…
steipete Jun 11, 2026
9d7967e
fix: keep Cost menu row width stable
steipete Jun 11, 2026
97b891f
Merge pull request #1413 from steipete/fix/cost-menu-width
steipete Jun 11, 2026
b4b1edc
chore: prepare 0.33.0 release
steipete Jun 11, 2026
6d156e7
style: apply SwiftFormat to utilization history tests
steipete Jun 11, 2026
6cf4225
docs: update appcast for 0.33.0
steipete Jun 11, 2026
2b76dc9
chore: start 0.33.1 development
steipete Jun 11, 2026
9015e94
docs: document Sparkle release key
steipete Jun 11, 2026
be4818c
feat: add Devin usage provider (#1264)
coygeek Jun 11, 2026
5745118
fix: show Cursor Full Disk Access hint first (#1419)
hhh2210 Jun 11, 2026
159d03c
fix: show Cursor legacy request quotas (#1420)
hhh2210 Jun 11, 2026
dd8cf8b
perf: reduce Codex cost refresh metadata work (#1430)
steipete Jun 11, 2026
408165b
Merge upstream CodexBar main through 0.33.1 development
claude Jun 11, 2026
6916dfc
Fix CI fallout from 0.33.1 upstream sync
claude Jun 11, 2026
a989088
Satisfy palette parity and parser-version audits for 0.33.1 sync
claude Jun 11, 2026
463053a
Update Japanese quit_app test expectation to QuotaKit branding
claude Jun 11, 2026
f2fdfcf
Adapt merged-icon deferral test to fork app-icon fallback
claude Jun 11, 2026
848538b
Pin token-account coalescing test to single-fetch path
claude Jun 11, 2026
1633ee2
Fix QuotaKit provider docs wording
ColumbusLabs Jun 11, 2026
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: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ on Columbus Labs QuotaKit releases and product-facing changes.
Codex account/auth hardening, MiniMax quota fixes, menu performance updates,
merged provider-switching hang fixes, Claude probe cleanup,
Antigravity/Alibaba/Cursor fixes, and additional Mac localizations.
- Synced upstream CodexBar Mac improvements through `0.33.1` development,
including a security fix that blocks credentialed provider redirects leaving
the original HTTPS origin, a new Devin usage provider, Cursor legacy
request-quota and Full Disk Access hint fixes, Copilot unlimited chat quota
display, Codex cost visibility without quotas, updated Claude usage pricing
and web session recovery, Doubao false-exhaustion fixes, cost scanner
threading and cancellation overhauls, broad menu performance and
width-stability work, a configurable terminal app for Open Terminal, expanded
MiMo browser support, and Japanese localization.

## 0.32.4.4 / iOS 1.11.1 — 2026-06-08

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ enum ProviderColorPalette {
(["groq", "groqcloud", "groqapi"], RawColor(red: 245 / 255, green: 104 / 255, blue: 68 / 255)),
(["llmproxy"], RawColor(red: 36 / 255, green: 180 / 255, blue: 126 / 255)),
(["deepgram"], RawColor(red: 0.49, green: 0.23, blue: 0.93)),
(["devin"], RawColor(red: 70 / 255, green: 180 / 255, blue: 130 / 255)),
]

var table: [String: RawColor] = [:]
Expand Down
45 changes: 41 additions & 4 deletions Sources/CodexBar/ClickToCopyOverlay.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import AppKit
import SwiftUI

@MainActor
enum MenuPasteboardCopy {
typealias DeferredAction = @MainActor @Sendable () -> Void
typealias Scheduler = @MainActor @Sendable (@escaping DeferredAction) -> Void
typealias Writer = @MainActor @Sendable (String) -> Void

static func perform(
_ text: String,
scheduler: Scheduler = Self.schedule,
writer: @escaping Writer = Self.write,
completion: @escaping DeferredAction = {})
{
scheduler {
writer(text)
completion()
}
}

private static func schedule(_ action: @escaping DeferredAction) {
DispatchQueue.main.async(execute: action)
}

private static func write(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
}

struct ClickToCopyOverlay: NSViewRepresentable {
let copyText: String

Expand All @@ -9,15 +38,25 @@ struct ClickToCopyOverlay: NSViewRepresentable {
}

func updateNSView(_ nsView: ClickToCopyView, context: Context) {
// Guard against no-op writes to avoid AppKit view invalidation on every
// parent card SwiftUI diff (each MenuCardView body re-eval runs through
// .overlay { ClickToCopyOverlay(...) }, which calls updateNSView even
// when copyText is unchanged).
guard nsView.copyText != self.copyText else { return }
nsView.copyText = self.copyText
}
}

final class ClickToCopyView: NSView {
var copyText: String
private let copyAction: (String) -> Void

init(copyText: String) {
init(
copyText: String,
copyAction: @escaping (String) -> Void = { MenuPasteboardCopy.perform($0) })
{
self.copyText = copyText
self.copyAction = copyAction
super.init(frame: .zero)
self.wantsLayer = false
}
Expand All @@ -33,8 +72,6 @@ final class ClickToCopyView: NSView {

override func mouseDown(with event: NSEvent) {
_ = event
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(self.copyText, forType: .string)
self.copyAction(self.copyText)
}
}
135 changes: 86 additions & 49 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,47 +131,68 @@ struct CostHistoryChartMenuView: View {
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
ForEach(detail.rows) { row in
HStack(alignment: .top, spacing: 8) {
Rectangle()
.fill(row.accentColor)
.frame(
width: 2,
height: Self.accentHeight(for: row))
.padding(.top, 1)

VStack(alignment: .leading, spacing: 1) {
Text(row.title)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailTitleLineHeight, alignment: .leading)
if let subtitle = row.subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailSubtitleLineHeight, alignment: .leading)
if model.maxRenderedBreakdownRows > 0 {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: Self.detailSpacing) {
ForEach(detail.rows) { row in
HStack(alignment: .top, spacing: 8) {
Rectangle()
.fill(row.accentColor)
.frame(
width: 2,
height: Self.accentHeight(for: row))
.padding(.top, 1)

VStack(alignment: .leading, spacing: 1) {
Text(row.title)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailTitleLineHeight, alignment: .leading)
if let subtitle = row.subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
if let modeSubtitle = row.modeSubtitle {
Text(modeSubtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
}
}
.frame(height: Self.detailRowHeight(for: row), alignment: .leading)
}
if let modeSubtitle = row.modeSubtitle {
Text(modeSubtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailSubtitleLineHeight, alignment: .leading)
ForEach(
0..<max(model.maxRenderedBreakdownRows - detail.rows.count, 0),
id: \.self)
{ _ in
Text(" ")
.font(.caption)
.frame(height: Self.compactDetailRowHeight, alignment: .leading)
.opacity(0)
}
}
}
.frame(height: Self.detailRowHeight(for: row), alignment: .leading)
}
ForEach(0..<max(model.maxRenderedBreakdownRows - detail.rows.count, 0), id: \.self) { _ in
Text(" ")
.font(.caption)
.frame(height: Self.compactDetailRowHeight, alignment: .leading)
.opacity(0)
.scrollIndicators(
Self.detailRowsNeedScrolling(itemCount: detail.rows.count) ? .visible : .hidden)
.frame(
height: Self.detailRowsViewportHeight(
maxBreakdownRows: model.maxRenderedBreakdownRows,
maxRowsHeight: model.maxDetailRowsHeight),
alignment: .topLeading)
.id(self.selectedDateKey)
}
}
.frame(
Expand Down Expand Up @@ -211,7 +232,7 @@ struct CostHistoryChartMenuView: View {
}

private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
private static let maxVisibleDetailLines = 4
static let maxVisibleDetailLines = 4
private static let detailPrimaryLineHeight: CGFloat = 16
private static let detailTitleLineHeight: CGFloat = 16
private static let detailSubtitleLineHeight: CGFloat = 13
Expand Down Expand Up @@ -338,8 +359,8 @@ struct CostHistoryChartMenuView: View {
private static func renderedBreakdownRowsMetric(for entry: DailyEntry) -> (count: Int, height: CGFloat) {
guard let breakdown = entry.modelBreakdowns, !breakdown.isEmpty else { return (0, 0) }
let renderedRows = Array(
self.sortedBreakdown(breakdown)
.prefix(self.maxVisibleDetailLines))
self.orderedBreakdownItems(breakdown)
.prefix(self.detailViewportRowCount(itemCount: breakdown.count)))
let height = renderedRows.reduce(CGFloat(0)) { total, item in
total + self.detailRowHeight(hasModeSubtitle: Self.hasModeSubtitle(item))
}
Expand All @@ -353,8 +374,18 @@ struct CostHistoryChartMenuView: View {
private static func detailBlockHeight(maxBreakdownRows: Int, maxRowsHeight: CGFloat) -> CGFloat {
guard maxBreakdownRows > 0 else { return self.detailPrimaryLineHeight }
return self.detailPrimaryLineHeight +
maxRowsHeight +
(CGFloat(maxBreakdownRows) * self.detailSpacing)
self.detailRowsViewportHeight(
maxBreakdownRows: maxBreakdownRows,
maxRowsHeight: maxRowsHeight) +
self.detailSpacing
}

private static func detailRowsViewportHeight(
maxBreakdownRows: Int,
maxRowsHeight: CGFloat) -> CGFloat
{
guard maxBreakdownRows > 0 else { return 0 }
return maxRowsHeight + (CGFloat(maxBreakdownRows - 1) * self.detailSpacing)
}

private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
Expand Down Expand Up @@ -400,10 +431,9 @@ struct CostHistoryChartMenuView: View {
proxy: ChartProxy,
geo: GeometryProxy)
{
guard let location else {
if self.selectedDateKey != nil { self.selectedDateKey = nil }
return
}
// Keep the last hovered day selected when the pointer leaves the chart so the adjacent
// model-breakdown scroller remains interactive. The selection resets with the menu view.
guard let location else { return }

guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
Expand Down Expand Up @@ -457,8 +487,7 @@ struct CostHistoryChartMenuView: View {
guard let entry = model.entriesByDateKey[key] else { return [] }
guard let breakdown = entry.modelBreakdowns, !breakdown.isEmpty else { return [] }

return Self.sortedBreakdown(breakdown)
.prefix(Self.maxVisibleDetailLines)
return Self.orderedBreakdownItems(breakdown)
.enumerated()
.map { index, item in
DetailRow(
Expand All @@ -470,7 +499,7 @@ struct CostHistoryChartMenuView: View {
}
}

private static func sortedBreakdown(
static func orderedBreakdownItems(
_ breakdown: [CostUsageDailyReport.ModelBreakdown]) -> [CostUsageDailyReport.ModelBreakdown]
{
breakdown.sorted { lhs, rhs in
Expand All @@ -486,6 +515,14 @@ struct CostHistoryChartMenuView: View {
}
}

static func detailViewportRowCount(itemCount: Int) -> Int {
min(max(itemCount, 0), self.maxVisibleDetailLines)
}

static func detailRowsNeedScrolling(itemCount: Int) -> Bool {
itemCount > self.maxVisibleDetailLines
}

private func modelBreakdownTotalSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> String? {
UsageFormatter.modelCostDetail(
item.modelName,
Expand Down
28 changes: 28 additions & 0 deletions Sources/CodexBar/KeychainPromptCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ enum KeychainPromptCoordinator {
BrowserCookieKeychainPromptHandler.handler = { context in
self.presentBrowserCookiePrompt(context)
}
self.disableKeychainForUnbundledExecutableIfNeeded()
}

private static let unbundledExecutableCheckLock = NSLock()
private nonisolated(unsafe) static var didCheckUnbundledExecutable = false

static func disableKeychainForUnbundledExecutableIfNeeded() {
self.unbundledExecutableCheckLock.lock()
guard !self.didCheckUnbundledExecutable else {
self.unbundledExecutableCheckLock.unlock()
return
}
self.didCheckUnbundledExecutable = true
self.unbundledExecutableCheckLock.unlock()

let executablePath = Bundle.main.executableURL?.path ?? ""
guard Self.isUnbundledCodexBarExecutable(executablePath) else { return }
KeychainAccessGate.forceDisabledForProcess(reason: "unbundled-executable")
Self.log.warning(
"Unbundled CodexBar executable detected; disabling keychain access to avoid repeated prompts",
metadata: ["doc": "docs/DEVELOPMENT_SETUP.md"])
}

static func isUnbundledCodexBarExecutable(_ executablePath: String) -> Bool {
guard executablePath.hasPrefix("/") else { return false }
let executableURL = URL(fileURLWithPath: executablePath).standardizedFileURL
return executableURL.lastPathComponent == "CodexBar"
&& !executableURL.pathComponents.contains(where: { $0.hasSuffix(".app") })
}

private static func presentKeychainPrompt(_ context: KeychainPromptContext) {
Expand Down
15 changes: 14 additions & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ extension UsageMenuCardView.Model {
self.placeholder != nil
}

var usesStackedDetailLayout: Bool {
!self.metrics.isEmpty ||
self.creditsText != nil ||
self.providerCost != nil ||
self.tokenUsage != nil
}

static func progressColor(for provider: UsageProvider) -> Color {
if provider == .cursor || provider == .elevenlabs {
return Color(nsColor: .labelColor)
Expand All @@ -52,7 +59,7 @@ extension UsageMenuCardView.Model {
}

if input.snapshot == nil, !input.isRefreshing, input.lastError == nil {
return L("No usage yet")
return self.hasLocalCodexTokenUsage(input) ? nil : L("No usage yet")
}

return nil
Expand All @@ -70,6 +77,12 @@ extension UsageMenuCardView.Model {
return lastError
}

private static func hasLocalCodexTokenUsage(_ input: Input) -> Bool {
input.provider == .codex &&
input.tokenCostUsageEnabled &&
self.tokenUsageSnapshot(input: input) != nil
}

private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
let currentError = lastError ?? input.lastError
if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
Expand Down
Loading