Skip to content

Commit 8d7f1e5

Browse files
fix: compact menu usage rows (#1669)
* fix: remove dividers and extra spacing around Cost row * fix: compact Storage, Cost, and Subscription Utilization into native rows * test: update native cost menu assertions * fix: preserve storage menu accessibility title * fix: deduplicate native menu separators * fix: separate native rows from menu actions --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 8c4bdd6 commit 8d7f1e5

8 files changed

Lines changed: 176 additions & 68 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixed
66
- Memory pressure: avoid actor-isolation crashes when system callbacks arrive on a utility queue. Thanks @Zihao-Qi!
7+
- Menu: remove extra separators and spacing around Storage, Cost, and Subscription Utilization rows. Thanks @elijahfriedman!
78

89
## 0.37.0 — 2026-06-19
910

Sources/CodexBar/StatusItemController+CostMenuCard.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ extension StatusItemController {
4141
let visibleDetailLines = Self.costMenuVisibleDetailLines(
4242
tokenUsage: model.tokenUsage,
4343
hasSubmenu: submenu != nil)
44-
guard self.menuCardRenderingEnabledForController else {
44+
guard visibleDetailLines.isEmpty == false, self.menuCardRenderingEnabledForController else {
4545
return Self.makeNativeCostMenuCardItem(
4646
visibleDetailLines: visibleDetailLines,
4747
tooltipLines: tooltipLines,

Sources/CodexBar/StatusItemController+Menu.swift

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,9 @@ extension StatusItemController {
715715
_ = self.addCostHistorySubmenu(to: menu, provider: currentProvider)
716716
}
717717
}
718-
menu.addItem(.separator())
718+
if menu.items.last?.isSeparatorItem != true {
719+
menu.addItem(.separator())
720+
}
719721
}
720722

721723
func addPrimaryMenuContent(
@@ -1206,6 +1208,10 @@ extension StatusItemController {
12061208
let sectionSpacing = CGFloat(6)
12071209
let usageBottomPadding = bottomPadding
12081210
let creditsBottomPadding = bottomPadding
1211+
func addSectionSeparator() {
1212+
guard menu.items.last?.isSeparatorItem != true else { return }
1213+
menu.addItem(.separator())
1214+
}
12091215

12101216
if hasUsageBlock {
12111217
let usageView = UsageMenuCardHeaderAndUsageSectionView(
@@ -1241,18 +1247,18 @@ extension StatusItemController {
12411247
}
12421248

12431249
if hasStorage || hasCredits || hasExtraUsage || hasCost {
1244-
menu.addItem(.separator())
1250+
addSectionSeparator()
12451251
}
12461252

12471253
if self.addStorageMenuCardSection(to: menu, provider: provider, width: width),
1248-
hasCredits || hasExtraUsage || hasCost
1254+
hasCredits || hasExtraUsage
12491255
{
1250-
menu.addItem(.separator())
1256+
addSectionSeparator()
12511257
}
12521258

12531259
if hasCredits {
12541260
if hasExtraUsage || hasCost {
1255-
menu.addItem(.separator())
1261+
addSectionSeparator()
12561262
}
12571263
let creditsView = UsageMenuCardCreditsSectionView(
12581264
model: model,
@@ -1274,7 +1280,7 @@ extension StatusItemController {
12741280
}
12751281
if hasExtraUsage {
12761282
if hasCredits {
1277-
menu.addItem(.separator())
1283+
addSectionSeparator()
12781284
}
12791285
let extraUsageSubmenu = self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width)
12801286
let extraUsageView = UsageMenuCardExtraUsageSectionView(
@@ -1292,7 +1298,7 @@ extension StatusItemController {
12921298
}
12931299
if hasCost {
12941300
if hasCredits || hasExtraUsage {
1295-
menu.addItem(.separator())
1301+
addSectionSeparator()
12961302
}
12971303
let costSubmenu = webItems.hasCostHistory ? self
12981304
.makeCostHistorySubmenu(provider: provider, width: width) : nil
@@ -1306,19 +1312,18 @@ extension StatusItemController {
13061312
@discardableResult
13071313
func addStorageMenuCardSection(to menu: NSMenu, provider: UsageProvider, width: CGFloat) -> Bool {
13081314
guard let storageText = self.store.storageFootprintText(for: provider) else { return false }
1309-
let storageView = StorageMenuCardSectionView(
1310-
storageText: storageText,
1311-
topPadding: 6,
1312-
bottomPadding: 6,
1313-
width: width)
13141315
let storageSubmenu = self.makeStorageBreakdownSubmenu(provider: provider, width: width)
1315-
menu.addItem(self.makeMenuCardItem(
1316-
storageView,
1317-
id: "menuCardStorage",
1318-
width: width,
1319-
heightCacheScope: provider.rawValue,
1320-
heightCacheFingerprint: UsageMenuCardView.Model.heightFingerprintField("storage", storageText),
1321-
submenu: storageSubmenu))
1316+
let menuFont = NSFont.menuFont(ofSize: 0)
1317+
let title = NSMutableAttributedString(string: L("Storage"), attributes: [.font: menuFont])
1318+
title.append(NSAttributedString(
1319+
string: " \(storageText)",
1320+
attributes: [.font: menuFont, .foregroundColor: NSColor.secondaryLabelColor]))
1321+
let item = NSMenuItem(title: L("Storage"), action: nil, keyEquivalent: "")
1322+
item.attributedTitle = title
1323+
item.isEnabled = storageSubmenu != nil
1324+
item.representedObject = "menuCardStorage"
1325+
item.submenu = storageSubmenu
1326+
menu.addItem(item)
13221327
return true
13231328
}
13241329

Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,10 @@ extension StatusItemController {
1212
@discardableResult
1313
func addUsageHistoryMenuItemIfNeeded(to menu: NSMenu, provider: UsageProvider, width: CGFloat) -> Bool {
1414
guard let submenu = self.makeUsageHistorySubmenu(provider: provider, width: width) else { return false }
15-
let item = self.makeMenuCardItem(
16-
HStack(spacing: 0) {
17-
Text(L("Subscription Utilization"))
18-
.font(.system(size: NSFont.menuFont(ofSize: 0).pointSize))
19-
.lineLimit(1)
20-
.frame(maxWidth: .infinity, alignment: .leading)
21-
.padding(.leading, 20)
22-
.padding(.trailing, 28)
23-
.padding(.vertical, 6)
24-
},
25-
id: "usageHistorySubmenu",
26-
width: width,
27-
heightCacheScope: provider.rawValue,
28-
heightCacheFingerprint: "usageHistorySubmenu:\(provider.rawValue)",
29-
submenu: submenu,
30-
submenuIndicatorAlignment: .trailing,
31-
submenuIndicatorTopPadding: 0)
15+
let item = NSMenuItem(title: L("Subscription Utilization"), action: nil, keyEquivalent: "")
16+
item.isEnabled = true
17+
item.representedObject = "usageHistorySubmenu"
18+
item.submenu = submenu
3219
menu.addItem(item)
3320
return true
3421
}

Sources/CodexBar/StorageBreakdownMenuView.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,6 @@ import AppKit
22
import CodexBarCore
33
import SwiftUI
44

5-
struct StorageMenuCardSectionView: View {
6-
let storageText: String
7-
let topPadding: CGFloat
8-
let bottomPadding: CGFloat
9-
let width: CGFloat
10-
11-
var body: some View {
12-
HStack(spacing: 6) {
13-
Text(L("Storage"))
14-
.font(.body)
15-
Text(self.storageText)
16-
.font(.body)
17-
.foregroundStyle(.secondary)
18-
Spacer(minLength: 0)
19-
}
20-
.padding(.horizontal, UsageMenuCardLayout.horizontalPadding)
21-
.padding(.top, self.topPadding)
22-
.padding(.bottom, self.bottomPadding)
23-
.frame(width: self.width, alignment: .leading)
24-
}
25-
}
26-
275
struct StorageBreakdownMenuView: View {
286
let footprint: ProviderStorageFootprint
297
let width: CGFloat

Tests/CodexBarTests/StatusMenuCostMenuCardTests.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,19 @@ struct StatusMenuCostMenuCardTests {
9595
errorLine: nil,
9696
errorCopyText: nil)
9797
let model = self.makeModel(tokenUsage: tokenUsage)
98-
let submenu = NSMenu()
9998

99+
// No history submenu — detail lines are visible and must be clipped to the row width.
100100
let item = controller.makeCostMenuCardItem(
101101
model: model,
102-
submenu: submenu,
102+
submenu: nil,
103103
width: width)
104104
let view = try #require(item.view)
105105

106106
#expect(view is any MenuCardMeasuring)
107107
#expect(abs(view.frame.width - width) <= 0.5)
108108
#expect(item.title == "Cost")
109109
#expect(item.toolTip?.contains("$52,431.09") == true)
110-
#expect(item.submenu === submenu)
111-
#expect(item.target === controller)
112-
#expect(item.action.map(NSStringFromSelector) == "menuCardNoOp:")
110+
#expect(item.submenu == nil)
113111
}
114112

115113
private func makeSettings() -> SettingsStore {

Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,36 @@ import Testing
66
@MainActor
77
@Suite(.serialized)
88
struct StatusMenuHostedSubmenuRefreshTests {
9+
@Test
10+
func `storage native row preserves its plain menu title`() throws {
11+
let settings = Self.makeSettings()
12+
settings.providerStorageFootprintsEnabled = true
13+
let fetcher = UsageFetcher()
14+
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
15+
Self.seedStorageFootprint(in: store)
16+
17+
let controller = StatusItemController(
18+
store: store,
19+
settings: settings,
20+
account: fetcher.loadAccountInfo(),
21+
updater: DisabledUpdaterController(),
22+
preferencesSelection: PreferencesSelection(),
23+
statusBar: .system)
24+
defer { controller.releaseStatusItemsForTesting() }
25+
26+
let menu = NSMenu()
27+
#expect(controller.addStorageMenuCardSection(
28+
to: menu,
29+
provider: .claude,
30+
width: StatusItemController.menuCardBaseWidth))
31+
let item = try #require(menu.items.first)
32+
#expect(item.title.hasPrefix(L("Storage")))
33+
#expect(item.title == item.attributedTitle?.string)
34+
#expect(item.view == nil)
35+
#expect(item.isEnabled)
36+
#expect(item.submenu != nil)
37+
}
38+
939
@Test
1040
func `open parent menu defers data rebuild until parent tracking ends`() async throws {
1141
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
@@ -43,11 +73,10 @@ struct StatusMenuHostedSubmenuRefreshTests {
4373
controller.menuVersions[parentKey] = controller.menuContentVersion
4474

4575
let costItem = try #require(menu.items.first { ($0.representedObject as? String) == "menuCardCost" })
46-
#expect(costItem.view is any MenuCardMeasuring)
76+
#expect(costItem.view == nil)
77+
#expect(costItem.title == StatusItemController.costMenuTitle)
78+
#expect(costItem.isEnabled)
4779
let submenu = try #require(costItem.submenu)
48-
let submenuAction = try #require(costItem.action)
49-
#expect(NSStringFromSelector(submenuAction) == "menuCardNoOp:")
50-
#expect(costItem.target === controller)
5180
#expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
5281
#expect(submenu.minimumWidth >= StatusItemController.menuCardBaseWidth)
5382
#expect(submenu.items.first?.view == nil)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import AppKit
2+
import CodexBarCore
3+
import Testing
4+
@testable import CodexBar
5+
6+
@MainActor
7+
@Suite(.serialized)
8+
struct StatusMenuNativeSectionSpacingTests {
9+
@Test
10+
func `storage credits and cost never create adjacent separators`() throws {
11+
let previousRendering = StatusItemController.menuCardRenderingEnabled
12+
StatusItemController.menuCardRenderingEnabled = true
13+
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
14+
15+
let settings = self.makeSettings()
16+
settings.statusChecksEnabled = false
17+
settings.refreshFrequency = .manual
18+
settings.mergeIcons = true
19+
settings.selectedMenuProvider = .codex
20+
settings.costUsageEnabled = true
21+
settings.providerStorageFootprintsEnabled = true
22+
self.enableOnlyCodex(settings)
23+
24+
let fetcher = UsageFetcher()
25+
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
26+
let storageRoot = "/Users/test/.codex"
27+
store.providerStorageFootprints[.codex] = ProviderStorageFootprint(
28+
provider: .codex,
29+
totalBytes: 1024,
30+
paths: [storageRoot],
31+
missingPaths: [],
32+
unreadablePaths: [],
33+
components: [.init(path: storageRoot, totalBytes: 1024)],
34+
updatedAt: Date())
35+
store.credits = CreditsSnapshot(remaining: 100, events: [], updatedAt: Date())
36+
store.openAIDashboard = OpenAIDashboardSnapshot(
37+
signedInEmail: "user@example.com",
38+
codeReviewRemainingPercent: 100,
39+
creditEvents: [],
40+
dailyBreakdown: [],
41+
usageBreakdown: [],
42+
creditsPurchaseURL: nil,
43+
updatedAt: Date())
44+
store.openAIDashboardAttachmentAuthorized = true
45+
store.openAIDashboardRequiresLogin = false
46+
store._setTokenSnapshotForTesting(CostUsageTokenSnapshot(
47+
sessionTokens: 123,
48+
sessionCostUSD: 0.12,
49+
last30DaysTokens: 123,
50+
last30DaysCostUSD: 1.23,
51+
daily: [
52+
CostUsageDailyReport.Entry(
53+
date: "2025-12-23",
54+
inputTokens: nil,
55+
outputTokens: nil,
56+
totalTokens: 123,
57+
costUSD: 1.23,
58+
modelsUsed: nil,
59+
modelBreakdowns: nil),
60+
],
61+
updatedAt: Date()), provider: .codex)
62+
63+
let controller = StatusItemController(
64+
store: store,
65+
settings: settings,
66+
account: fetcher.loadAccountInfo(),
67+
updater: DisabledUpdaterController(),
68+
preferencesSelection: PreferencesSelection(),
69+
statusBar: .system)
70+
defer { controller.releaseStatusItemsForTesting() }
71+
72+
let menu = controller.makeMenu(for: .codex)
73+
controller.menuWillOpen(menu)
74+
let storageIndex = try #require(menu.items.firstIndex {
75+
($0.representedObject as? String) == "menuCardStorage"
76+
})
77+
let creditsIndex = try #require(menu.items.firstIndex {
78+
($0.representedObject as? String) == "menuCardCredits"
79+
})
80+
let costIndex = try #require(menu.items.firstIndex {
81+
($0.representedObject as? String) == "menuCardCost"
82+
})
83+
#expect(storageIndex < creditsIndex)
84+
#expect(creditsIndex < costIndex)
85+
#expect(menu.items[costIndex + 1].isSeparatorItem)
86+
#expect(!zip(menu.items, menu.items.dropFirst()).contains { first, second in
87+
first.isSeparatorItem && second.isSeparatorItem
88+
})
89+
}
90+
91+
private func makeSettings() -> SettingsStore {
92+
let suite = "StatusMenuNativeSectionSpacingTests-\(UUID().uuidString)"
93+
let defaults = UserDefaults(suiteName: suite)!
94+
defaults.removePersistentDomain(forName: suite)
95+
let settings = SettingsStore(
96+
userDefaults: defaults,
97+
configStore: testConfigStore(suiteName: suite),
98+
zaiTokenStore: NoopZaiTokenStore(),
99+
syntheticTokenStore: NoopSyntheticTokenStore())
100+
settings.providerDetectionCompleted = true
101+
return settings
102+
}
103+
104+
private func enableOnlyCodex(_ settings: SettingsStore) {
105+
for provider in UsageProvider.allCases {
106+
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
107+
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)