Skip to content

Commit 4484fad

Browse files
authored
fix: detect Tahoe no-window menu block (#1954)
1 parent dcccce8 commit 4484fad

6 files changed

Lines changed: 253 additions & 15 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Kimi K2: reject non-finite credit and token values before they reach menus, CLI output, or widgets. Thanks @joeVenner!
2222
- Kimi: call the current `GetSubscriptionStats` membership endpoint so the Monthly subscription quota is populated again. Thanks @skyzer!
2323
- Kimi: show the five-hour rate limit before the weekly quota while preserving existing menu-bar metric preferences. Thanks @Zihao-Qi!
24+
- Menu bar: detect Tahoe's blocked no-window state at startup when macOS still records the icon as enabled, so affected users receive recovery guidance instead of a silently missing icon (#1945). Thanks @mmyyfirstb!
2425

2526
## 0.40.0 — 2026-07-05
2627

Sources/CodexBar/MenuBarStatusItemDefaultsRepair.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,28 @@ enum MenuBarStatusItemDefaultsRepair {
2727
return itemName.hasPrefix(self.legacyAutosavePrefix) || self.isDefaultStatusItemName(itemName)
2828
}
2929

30+
static func visibilityDefault(defaults: UserDefaults, autosaveName: String) -> Bool? {
31+
guard !autosaveName.isEmpty else { return nil }
32+
return self.boolValue(defaults.object(forKey: self.visibilityPrefix + autosaveName))
33+
}
34+
3035
private static func isDefaultStatusItemName(_ itemName: String) -> Bool {
3136
guard itemName.hasPrefix("Item-") else { return false }
3237
return itemName.dropFirst("Item-".count).allSatisfy(\.isNumber)
3338
}
3439

3540
private static func isFalse(_ value: Any?) -> Bool {
41+
self.boolValue(value) == false
42+
}
43+
44+
private static func boolValue(_ value: Any?) -> Bool? {
3645
switch value {
3746
case let number as NSNumber:
38-
!number.boolValue
47+
number.boolValue
3948
case let bool as Bool:
40-
!bool
49+
bool
4150
default:
42-
false
51+
nil
4352
}
4453
}
4554
}

Sources/CodexBar/MenuBarVisibilityWatcher.swift

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ extension StatusItemVisibilitySnapshot: CustomStringConvertible {
3434
}
3535
}
3636

37+
struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
38+
let autosaveName: String
39+
let expectsVisibility: Bool
40+
let visibilityDefault: Bool?
41+
let snapshot: StatusItemVisibilitySnapshot
42+
43+
var description: String {
44+
"name=\(self.autosaveName),expected=\(self.expectsVisibility),"
45+
+ "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
46+
}
47+
}
48+
3749
@MainActor
3850
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
3951
MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
@@ -113,14 +125,20 @@ enum MenuBarVisibilityWatcher {
113125

114126
static func hasAnyStartupRecoveryCandidate(
115127
snapshots: [StatusItemVisibilitySnapshot],
128+
evidence: [StatusItemStartupVisibilityEvidence] = [],
116129
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
117-
detectTahoeBlockedProxy: Bool = false)
130+
detectTahoeBlockedStatusItem: Bool = false)
118131
-> Bool
119132
{
120133
if self.hasAnyBlockedVisibleSnapshot(snapshots) {
121134
return true
122135
}
123-
guard detectTahoeBlockedProxy,
136+
if detectTahoeBlockedStatusItem,
137+
self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
138+
{
139+
return true
140+
}
141+
guard detectTahoeBlockedStatusItem,
124142
self.hasAnyDisplacedVisibleSnapshot(snapshots),
125143
windowSnapshots.contains(where: \.isTahoeBlockedProxy)
126144
else {
@@ -129,6 +147,24 @@ enum MenuBarVisibilityWatcher {
129147
return true
130148
}
131149

150+
static func hasAnyTahoeHiddenNoProxyCandidate(
151+
evidence: [StatusItemStartupVisibilityEvidence],
152+
windowSnapshots: [MenuBarStatusItemWindowSnapshot])
153+
-> Bool
154+
{
155+
evidence.contains { item in
156+
// Tahoe can destroy the Control Center scene while leaving its enabled default behind.
157+
// Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
158+
item.expectsVisibility
159+
&& item.visibilityDefault == true
160+
&& !item.snapshot.isVisible
161+
&& !item.snapshot.hasWindow
162+
&& !windowSnapshots.contains {
163+
$0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
164+
}
165+
}
166+
}
167+
132168
@MainActor
133169
static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
134170
items.map { item in
@@ -145,15 +181,17 @@ enum MenuBarVisibilityWatcher {
145181
appLaunchedAt: Date,
146182
now: Date = Date(),
147183
snapshots: [StatusItemVisibilitySnapshot],
184+
evidence: [StatusItemStartupVisibilityEvidence] = [],
148185
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
149-
detectTahoeBlockedProxy: Bool = false)
186+
detectTahoeBlockedStatusItem: Bool = false)
150187
-> Bool
151188
{
152189
guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
153190
return self.hasAnyStartupRecoveryCandidate(
154191
snapshots: snapshots,
192+
evidence: evidence,
155193
windowSnapshots: windowSnapshots,
156-
detectTahoeBlockedProxy: detectTahoeBlockedProxy)
194+
detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
157195
}
158196

159197
static func shouldRefreshScreenChangePlacement(
@@ -215,14 +253,16 @@ extension StatusItemController {
215253
}
216254

217255
private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
218-
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
256+
let evidence = self.startupStatusItemVisibilityEvidence()
257+
let snapshots = evidence.map(\.snapshot)
219258
let windowSnapshots = self.statusItemWindowSnapshots()
220259
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
221260
appLaunchedAt: appLaunchedAt,
222261
now: now,
223262
snapshots: snapshots,
263+
evidence: evidence,
224264
windowSnapshots: windowSnapshots,
225-
detectTahoeBlockedProxy: self.canDetectTahoeBlockedProxy)
265+
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
226266
else {
227267
return
228268
}
@@ -231,18 +271,21 @@ extension StatusItemController {
231271
"Status item failed to materialize or remained detached; recreating status items",
232272
metadata: [
233273
"snapshots": snapshots.map(\.description).joined(separator: " | "),
274+
"evidence": evidence.map(\.description).joined(separator: " | "),
234275
"windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
235276
])
236277
self.recreateStatusItemsForVisibilityRecovery()
237278

238-
let recoveredSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
279+
let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
280+
let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
239281
let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
240282
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
241283
appLaunchedAt: appLaunchedAt,
242284
now: now,
243285
snapshots: recoveredSnapshots,
286+
evidence: recoveredEvidence,
244287
windowSnapshots: recoveredWindowSnapshots,
245-
detectTahoeBlockedProxy: self.canDetectTahoeBlockedProxy)
288+
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
246289
else {
247290
self.menuLogger.info(
248291
"Status item materialized after recreation",
@@ -254,6 +297,7 @@ extension StatusItemController {
254297
"Status item still unavailable after recreation",
255298
metadata: [
256299
"snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
300+
"evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
257301
"windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
258302
])
259303
guard #available(macOS 26.0, *),
@@ -382,7 +426,20 @@ extension StatusItemController {
382426
[self.statusItem] + Array(self.statusItems.values)
383427
}
384428

385-
private var canDetectTahoeBlockedProxy: Bool {
429+
private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
430+
self.startupVisibilityStatusItems.map { item in
431+
let autosaveName = item.autosaveName ?? ""
432+
return StatusItemStartupVisibilityEvidence(
433+
autosaveName: autosaveName,
434+
expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
435+
visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
436+
defaults: self.settings.userDefaults,
437+
autosaveName: autosaveName),
438+
snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
439+
}
440+
}
441+
442+
private var canDetectTahoeBlockedStatusItem: Bool {
386443
if #available(macOS 26.0, *) {
387444
return true
388445
}

Sources/CodexBar/StatusItemController.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
130130
let menuRefreshEnabledForController: Bool
131131
var statusItem: NSStatusItem
132132
var statusItems: [UsageProvider: NSStatusItem] = [:]
133+
/// App intent survives Tahoe changing `NSStatusItem.isVisible` after Control Center rejects its scene.
134+
var expectedVisibleStatusItemAutosaveNames: Set<String> = []
133135
var lastMenuProvider: UsageProvider?
134136
var menuProviders: [ObjectIdentifier: UsageProvider] = [:]
135137
var menuSession = MenuSessionCoordinator<ObjectIdentifier>()
@@ -743,8 +745,13 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
743745
let anyEnabled = !self.store.enabledProvidersForDisplay().isEmpty
744746
let force = self.store.debugForceAnimation
745747
let mergeIcons = self.shouldMergeIcons
748+
var expectedVisibleAutosaveNames: Set<String> = []
746749
if mergeIcons {
747-
self.statusItem.isVisible = anyEnabled || force
750+
let shouldBeVisible = anyEnabled || force
751+
self.statusItem.isVisible = shouldBeVisible
752+
if shouldBeVisible {
753+
expectedVisibleAutosaveNames.insert(self.statusItem.autosaveName)
754+
}
748755
for provider in Array(self.statusItems.keys) {
749756
self.removeProviderStatusItem(for: provider)
750757
}
@@ -758,12 +765,14 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
758765
if shouldBeVisible {
759766
let item = self.lazyStatusItem(for: provider)
760767
item.isVisible = true
768+
expectedVisibleAutosaveNames.insert(item.autosaveName)
761769
} else {
762770
self.removeProviderStatusItem(for: provider)
763771
}
764772
}
765773
self.attachMenus(fallback: fallback)
766774
}
775+
self.expectedVisibleStatusItemAutosaveNames = expectedVisibleAutosaveNames
767776
self.updateAnimationState()
768777
self.updateBlinkingState()
769778
}

Tests/CodexBarTests/MenuBarVisibilityWatcherTests.swift

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,147 @@ struct MenuBarVisibilityWatcherTests {
244244
now: launchedAt.addingTimeInterval(2),
245245
snapshots: [detachedProxy],
246246
windowSnapshots: [blockedWindow],
247-
detectTahoeBlockedProxy: true))
247+
detectTahoeBlockedStatusItem: true))
248+
}
249+
250+
@Test
251+
func `startup recovery retries expected hidden Tahoe item with enabled default and no window`() {
252+
let launchedAt = Date(timeIntervalSince1970: 1000)
253+
let hidden = StatusItemVisibilitySnapshot(
254+
isVisible: false,
255+
hasButton: true,
256+
hasWindow: false,
257+
hasScreen: false,
258+
buttonWidth: 76)
259+
let evidence = StatusItemStartupVisibilityEvidence(
260+
autosaveName: "codexbar-merged",
261+
expectsVisibility: true,
262+
visibilityDefault: true,
263+
snapshot: hidden)
264+
265+
#expect(MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
266+
appLaunchedAt: launchedAt,
267+
now: launchedAt.addingTimeInterval(2),
268+
snapshots: [hidden],
269+
evidence: [evidence],
270+
detectTahoeBlockedStatusItem: true))
271+
}
272+
273+
@Test
274+
func `startup recovery ignores hidden Tahoe item without app and defaults visibility agreement`() {
275+
let launchedAt = Date(timeIntervalSince1970: 1000)
276+
let hidden = StatusItemVisibilitySnapshot(
277+
isVisible: false,
278+
hasButton: true,
279+
hasWindow: false,
280+
hasScreen: false,
281+
buttonWidth: 76)
282+
let intentionallyHidden = StatusItemStartupVisibilityEvidence(
283+
autosaveName: "codexbar-merged",
284+
expectsVisibility: false,
285+
visibilityDefault: true,
286+
snapshot: hidden)
287+
let disabledByUser = StatusItemStartupVisibilityEvidence(
288+
autosaveName: "codexbar-merged",
289+
expectsVisibility: true,
290+
visibilityDefault: false,
291+
snapshot: hidden)
292+
let unknownDefault = StatusItemStartupVisibilityEvidence(
293+
autosaveName: "codexbar-merged",
294+
expectsVisibility: true,
295+
visibilityDefault: nil,
296+
snapshot: hidden)
297+
298+
for evidence in [intentionallyHidden, disabledByUser, unknownDefault] {
299+
#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
300+
appLaunchedAt: launchedAt,
301+
now: launchedAt.addingTimeInterval(2),
302+
snapshots: [hidden],
303+
evidence: [evidence],
304+
detectTahoeBlockedStatusItem: true))
305+
}
306+
}
307+
308+
@Test
309+
func `startup recovery ignores hidden item when matching window still exists`() {
310+
let launchedAt = Date(timeIntervalSince1970: 1000)
311+
let hidden = StatusItemVisibilitySnapshot(
312+
isVisible: false,
313+
hasButton: true,
314+
hasWindow: false,
315+
hasScreen: false,
316+
buttonWidth: 76)
317+
let evidence = StatusItemStartupVisibilityEvidence(
318+
autosaveName: "codexbar-merged",
319+
expectsVisibility: true,
320+
visibilityDefault: true,
321+
snapshot: hidden)
322+
let existingWindow = MenuBarStatusItemWindowSnapshot(
323+
name: "codexbar-merged",
324+
ownerName: "Control Center",
325+
bounds: CGRect(x: 1500, y: 0, width: 76, height: 24),
326+
isOnscreen: true,
327+
displayBounds: CGRect(x: 0, y: 0, width: 2056, height: 1329))
328+
329+
#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
330+
appLaunchedAt: launchedAt,
331+
now: launchedAt.addingTimeInterval(2),
332+
snapshots: [hidden],
333+
evidence: [evidence],
334+
windowSnapshots: [existingWindow],
335+
detectTahoeBlockedStatusItem: true))
336+
}
337+
338+
@Test
339+
func `startup recovery ignores stale hidden matching window record`() {
340+
let launchedAt = Date(timeIntervalSince1970: 1000)
341+
let hidden = StatusItemVisibilitySnapshot(
342+
isVisible: false,
343+
hasButton: true,
344+
hasWindow: false,
345+
hasScreen: false,
346+
buttonWidth: 76)
347+
let evidence = StatusItemStartupVisibilityEvidence(
348+
autosaveName: "codexbar-merged",
349+
expectsVisibility: true,
350+
visibilityDefault: true,
351+
snapshot: hidden)
352+
let staleWindow = MenuBarStatusItemWindowSnapshot(
353+
name: "codexbar-merged",
354+
ownerName: "Control Center",
355+
bounds: CGRect(x: 1500, y: 0, width: 76, height: 24),
356+
isOnscreen: false,
357+
displayBounds: CGRect(x: 0, y: 0, width: 2056, height: 1329))
358+
359+
#expect(MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
360+
appLaunchedAt: launchedAt,
361+
now: launchedAt.addingTimeInterval(2),
362+
snapshots: [hidden],
363+
evidence: [evidence],
364+
windowSnapshots: [staleWindow],
365+
detectTahoeBlockedStatusItem: true))
366+
}
367+
368+
@Test
369+
func `startup recovery keeps hidden no-window detection Tahoe only`() {
370+
let launchedAt = Date(timeIntervalSince1970: 1000)
371+
let hidden = StatusItemVisibilitySnapshot(
372+
isVisible: false,
373+
hasButton: true,
374+
hasWindow: false,
375+
hasScreen: false,
376+
buttonWidth: 76)
377+
let evidence = StatusItemStartupVisibilityEvidence(
378+
autosaveName: "codexbar-merged",
379+
expectsVisibility: true,
380+
visibilityDefault: true,
381+
snapshot: hidden)
382+
383+
#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
384+
appLaunchedAt: launchedAt,
385+
now: launchedAt.addingTimeInterval(2),
386+
snapshots: [hidden],
387+
evidence: [evidence]))
248388
}
249389

250390
@Test
@@ -262,7 +402,7 @@ struct MenuBarVisibilityWatcherTests {
262402
appLaunchedAt: launchedAt,
263403
now: launchedAt.addingTimeInterval(2),
264404
snapshots: [managed],
265-
detectTahoeBlockedProxy: true))
405+
detectTahoeBlockedStatusItem: true))
266406
}
267407

268408
@Test

0 commit comments

Comments
 (0)