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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- Kimi K2: reject invalid and out-of-range numeric timestamps while preserving valid second and millisecond values. Thanks @joeVenner!
- Kimi K2: reject non-finite credit and token values before they reach menus, CLI output, or widgets. Thanks @joeVenner!
- Kimi: show the five-hour rate limit before the weekly quota while preserving existing menu-bar metric preferences. Thanks @Zihao-Qi!
- 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!

## 0.40.0 — 2026-07-05

Expand Down
15 changes: 12 additions & 3 deletions Sources/CodexBar/MenuBarStatusItemDefaultsRepair.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,28 @@ enum MenuBarStatusItemDefaultsRepair {
return itemName.hasPrefix(self.legacyAutosavePrefix) || self.isDefaultStatusItemName(itemName)
}

static func visibilityDefault(defaults: UserDefaults, autosaveName: String) -> Bool? {
guard !autosaveName.isEmpty else { return nil }
return self.boolValue(defaults.object(forKey: self.visibilityPrefix + autosaveName))
}

private static func isDefaultStatusItemName(_ itemName: String) -> Bool {
guard itemName.hasPrefix("Item-") else { return false }
return itemName.dropFirst("Item-".count).allSatisfy(\.isNumber)
}

private static func isFalse(_ value: Any?) -> Bool {
self.boolValue(value) == false
}

private static func boolValue(_ value: Any?) -> Bool? {
switch value {
case let number as NSNumber:
!number.boolValue
number.boolValue
case let bool as Bool:
!bool
bool
default:
false
nil
}
}
}
75 changes: 66 additions & 9 deletions Sources/CodexBar/MenuBarVisibilityWatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ extension StatusItemVisibilitySnapshot: CustomStringConvertible {
}
}

struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
let autosaveName: String
let expectsVisibility: Bool
let visibilityDefault: Bool?
let snapshot: StatusItemVisibilitySnapshot

var description: String {
"name=\(self.autosaveName),expected=\(self.expectsVisibility),"
+ "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
}
}

@MainActor
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
Expand Down Expand Up @@ -113,14 +125,20 @@ enum MenuBarVisibilityWatcher {

static func hasAnyStartupRecoveryCandidate(
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedProxy: Bool = false)
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
if self.hasAnyBlockedVisibleSnapshot(snapshots) {
return true
}
guard detectTahoeBlockedProxy,
if detectTahoeBlockedStatusItem,
self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
{
return true
}
guard detectTahoeBlockedStatusItem,
self.hasAnyDisplacedVisibleSnapshot(snapshots),
windowSnapshots.contains(where: \.isTahoeBlockedProxy)
else {
Expand All @@ -129,6 +147,24 @@ enum MenuBarVisibilityWatcher {
return true
}

static func hasAnyTahoeHiddenNoProxyCandidate(
evidence: [StatusItemStartupVisibilityEvidence],
windowSnapshots: [MenuBarStatusItemWindowSnapshot])
-> Bool
{
evidence.contains { item in
// Tahoe can destroy the Control Center scene while leaving its enabled default behind.
// Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
item.expectsVisibility
&& item.visibilityDefault == true
&& !item.snapshot.isVisible
&& !item.snapshot.hasWindow
&& !windowSnapshots.contains {
$0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
}
}
}

@MainActor
static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
items.map { item in
Expand All @@ -145,15 +181,17 @@ enum MenuBarVisibilityWatcher {
appLaunchedAt: Date,
now: Date = Date(),
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedProxy: Bool = false)
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
return self.hasAnyStartupRecoveryCandidate(
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedProxy: detectTahoeBlockedProxy)
detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
}

static func shouldRefreshScreenChangePlacement(
Expand Down Expand Up @@ -215,14 +253,16 @@ extension StatusItemController {
}

private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
let evidence = self.startupStatusItemVisibilityEvidence()
let snapshots = evidence.map(\.snapshot)
let windowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedProxy: self.canDetectTahoeBlockedProxy)
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
return
}
Expand All @@ -231,18 +271,21 @@ extension StatusItemController {
"Status item failed to materialize or remained detached; recreating status items",
metadata: [
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"evidence": evidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
])
self.recreateStatusItemsForVisibilityRecovery()

let recoveredSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: recoveredSnapshots,
evidence: recoveredEvidence,
windowSnapshots: recoveredWindowSnapshots,
detectTahoeBlockedProxy: self.canDetectTahoeBlockedProxy)
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
self.menuLogger.info(
"Status item materialized after recreation",
Expand All @@ -254,6 +297,7 @@ extension StatusItemController {
"Status item still unavailable after recreation",
metadata: [
"snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
"evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
])
guard #available(macOS 26.0, *),
Expand Down Expand Up @@ -382,7 +426,20 @@ extension StatusItemController {
[self.statusItem] + Array(self.statusItems.values)
}

private var canDetectTahoeBlockedProxy: Bool {
private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
self.startupVisibilityStatusItems.map { item in
let autosaveName = item.autosaveName ?? ""
return StatusItemStartupVisibilityEvidence(
autosaveName: autosaveName,
expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
defaults: self.settings.userDefaults,
autosaveName: autosaveName),
snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}
}

private var canDetectTahoeBlockedStatusItem: Bool {
if #available(macOS 26.0, *) {
return true
}
Expand Down
11 changes: 10 additions & 1 deletion Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
let menuRefreshEnabledForController: Bool
var statusItem: NSStatusItem
var statusItems: [UsageProvider: NSStatusItem] = [:]
/// App intent survives Tahoe changing `NSStatusItem.isVisible` after Control Center rejects its scene.
var expectedVisibleStatusItemAutosaveNames: Set<String> = []
var lastMenuProvider: UsageProvider?
var menuProviders: [ObjectIdentifier: UsageProvider] = [:]
var menuSession = MenuSessionCoordinator<ObjectIdentifier>()
Expand Down Expand Up @@ -743,8 +745,13 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
let anyEnabled = !self.store.enabledProvidersForDisplay().isEmpty
let force = self.store.debugForceAnimation
let mergeIcons = self.shouldMergeIcons
var expectedVisibleAutosaveNames: Set<String> = []
if mergeIcons {
self.statusItem.isVisible = anyEnabled || force
let shouldBeVisible = anyEnabled || force
self.statusItem.isVisible = shouldBeVisible
if shouldBeVisible {
expectedVisibleAutosaveNames.insert(self.statusItem.autosaveName)
}
for provider in Array(self.statusItems.keys) {
self.removeProviderStatusItem(for: provider)
}
Expand All @@ -758,12 +765,14 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
if shouldBeVisible {
let item = self.lazyStatusItem(for: provider)
item.isVisible = true
expectedVisibleAutosaveNames.insert(item.autosaveName)
} else {
self.removeProviderStatusItem(for: provider)
}
}
self.attachMenus(fallback: fallback)
}
self.expectedVisibleStatusItemAutosaveNames = expectedVisibleAutosaveNames
self.updateAnimationState()
self.updateBlinkingState()
}
Expand Down
144 changes: 142 additions & 2 deletions Tests/CodexBarTests/MenuBarVisibilityWatcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,147 @@ struct MenuBarVisibilityWatcherTests {
now: launchedAt.addingTimeInterval(2),
snapshots: [detachedProxy],
windowSnapshots: [blockedWindow],
detectTahoeBlockedProxy: true))
detectTahoeBlockedStatusItem: true))
}

@Test
func `startup recovery retries expected hidden Tahoe item with enabled default and no window`() {
let launchedAt = Date(timeIntervalSince1970: 1000)
let hidden = StatusItemVisibilitySnapshot(
isVisible: false,
hasButton: true,
hasWindow: false,
hasScreen: false,
buttonWidth: 76)
let evidence = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: true,
snapshot: hidden)

#expect(MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [hidden],
evidence: [evidence],
detectTahoeBlockedStatusItem: true))
}

@Test
func `startup recovery ignores hidden Tahoe item without app and defaults visibility agreement`() {
let launchedAt = Date(timeIntervalSince1970: 1000)
let hidden = StatusItemVisibilitySnapshot(
isVisible: false,
hasButton: true,
hasWindow: false,
hasScreen: false,
buttonWidth: 76)
let intentionallyHidden = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: false,
visibilityDefault: true,
snapshot: hidden)
let disabledByUser = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: false,
snapshot: hidden)
let unknownDefault = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: nil,
snapshot: hidden)

for evidence in [intentionallyHidden, disabledByUser, unknownDefault] {
#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [hidden],
evidence: [evidence],
detectTahoeBlockedStatusItem: true))
}
}

@Test
func `startup recovery ignores hidden item when matching window still exists`() {
let launchedAt = Date(timeIntervalSince1970: 1000)
let hidden = StatusItemVisibilitySnapshot(
isVisible: false,
hasButton: true,
hasWindow: false,
hasScreen: false,
buttonWidth: 76)
let evidence = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: true,
snapshot: hidden)
let existingWindow = MenuBarStatusItemWindowSnapshot(
name: "codexbar-merged",
ownerName: "Control Center",
bounds: CGRect(x: 1500, y: 0, width: 76, height: 24),
isOnscreen: true,
displayBounds: CGRect(x: 0, y: 0, width: 2056, height: 1329))

#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [hidden],
evidence: [evidence],
windowSnapshots: [existingWindow],
detectTahoeBlockedStatusItem: true))
}

@Test
func `startup recovery ignores stale hidden matching window record`() {
let launchedAt = Date(timeIntervalSince1970: 1000)
let hidden = StatusItemVisibilitySnapshot(
isVisible: false,
hasButton: true,
hasWindow: false,
hasScreen: false,
buttonWidth: 76)
let evidence = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: true,
snapshot: hidden)
let staleWindow = MenuBarStatusItemWindowSnapshot(
name: "codexbar-merged",
ownerName: "Control Center",
bounds: CGRect(x: 1500, y: 0, width: 76, height: 24),
isOnscreen: false,
displayBounds: CGRect(x: 0, y: 0, width: 2056, height: 1329))

#expect(MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [hidden],
evidence: [evidence],
windowSnapshots: [staleWindow],
detectTahoeBlockedStatusItem: true))
}

@Test
func `startup recovery keeps hidden no-window detection Tahoe only`() {
let launchedAt = Date(timeIntervalSince1970: 1000)
let hidden = StatusItemVisibilitySnapshot(
isVisible: false,
hasButton: true,
hasWindow: false,
hasScreen: false,
buttonWidth: 76)
let evidence = StatusItemStartupVisibilityEvidence(
autosaveName: "codexbar-merged",
expectsVisibility: true,
visibilityDefault: true,
snapshot: hidden)

#expect(!MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [hidden],
evidence: [evidence]))
}

@Test
Expand All @@ -262,7 +402,7 @@ struct MenuBarVisibilityWatcherTests {
appLaunchedAt: launchedAt,
now: launchedAt.addingTimeInterval(2),
snapshots: [managed],
detectTahoeBlockedProxy: true))
detectTahoeBlockedStatusItem: true))
}

@Test
Expand Down
Loading
Loading