Skip to content

Fix IceBar click flakiness on macOS 26 (notched + Control Center reparenting)#911

Open
tducp015 wants to merge 74 commits into
jordanbaird:mainfrom
tducp015:fix/sourcepid-cache-thrash
Open

Fix IceBar click flakiness on macOS 26 (notched + Control Center reparenting)#911
tducp015 wants to merge 74 commits into
jordanbaird:mainfrom
tducp015:fix/sourcepid-cache-thrash

Conversation

@tducp015

@tducp015 tducp015 commented Apr 7, 2026

Copy link
Copy Markdown

Summary

On macOS 26 (Tahoe), the IceBar exhibits "click flakiness": clicking the Ice control item often fails to open the bar, and clicking a hidden item inside the bar often does nothing. On notched MacBooks with menu bar accessories (e.g. NotchNook), the bar can also fail to populate at all, sitting forever on "Loading menu bar items…".

This PR fixes three independent root causes that all surface as the same symptom. They are split into three commits so each can be reviewed, reverted, or backported on its own.

Environment in which this was reproduced and verified

  • macOS 26.3.1 Tahoe, Apple silicon, MacBook display with notch + a secondary display
  • NotchNook installed (any menu bar accessory that paints near the corner of the bar should reproduce parts of this)
  • Ice 0.11.13-dev.2 / main

1. SourcePID cache thrash (commit 1)

MenuBarItemManager.uncheckedCacheItems set shouldClearCachedItemWindowIDs = true on every menu bar item whose sourcePID was nil. On macOS 26 there is always at least one such item (any app whose extras menu bar isn't exposed via Accessibility), so the cache invalidation became a feedback loop:

  1. cache refresh → item with nil sourcePID → invalidate window IDs
  2. window IDs invalidated → next refresh re-runs getMenuBarItems
  3. getMenuBarItems does an XPC roundtrip + an O(running apps) AX enumeration for every previously-failed item
  4. result still has nil sourcePID → goto 1

Under that thrash, IceBarPanel.show() raced with the background re-cache, and MenuBarSection.toggle()'s isHidden check against controlItem.state could end up calling hide() on a bar that had just been shown — clicks failing to open the bar about half the time.

Fix:

  • SourcePIDCache records failed lookups with a 30s TTL, skipping the AX scan on retry. The TTL entry is cleared whenever the running apps list changes, so newly-launched apps still get a chance to match.
  • MenuBarItemManager no longer invalidates the item window ID cache on a nil sourcePID. Items with stable UUID-based tags stay cached until a running-apps change gives them another chance to resolve.

2. Control-item recognition after Control Center reparenting (commit 2)

In macOS 26, Control Center re-parents virtually every menu bar item window, including Ice's own control items. kCGWindowOwnerPID becomes Control Center's PID for all of them, so Ice's control items end up flowing through the same source-PID lookup as everything else.

When that lookup fails (which it always does for Ice, because Ice as an LSUIElement app does not expose its NSStatusItems via AXExtrasMenuBar), MenuBarItemTag.Namespace.init(uncheckedItemWindow:sourcePID:) falls through to a synthetic .uuid(...) namespace. The title is still set correctly (Ice.ControlItem.Hidden etc.), but the namespace mismatch means the resulting tag is no longer equal to MenuBarItemTag.hiddenControlItem.

ControlItemPair.init(items:) then can't find the hidden control item, cacheItemsRegardless clears the entire menu bar item cache, the next refresh produces the same items with the same wrong tags, and the cache rebuild loops indefinitely. The IceBar never leaves "Loading menu bar items…".

Fix: in the macOS 26 namespace initializer, recognise Ice's own control items by their window title prefix (Ice.ControlItem.) and force the namespace to .ice regardless of what the source-PID lookup returned. As a related fallback, items whose owner is not Control Center (i.e. not reparented) now get their owner's bundle identifier instead of a UUID — useful for any well-behaved status item app whose AX extras menu bar lookup happens to fail.

I confirmed via diagnostic logging on macOS 26.3.1 that the failing items look like:

```
tag=66387C2B-...:Ice.ControlItem.Hidden ownerPID=15219 (Control Center) sourcePID=nil
```

before the fix, and:

```
Missing sourcePID for <com.jordanbaird.Ice:Ice.ControlItem.Visible (windowID: ...)>
```

(harmless "missing sourcePID" log, but the tag now matches) after the fix. The cache rebuild loop terminates after the very first attempt.

3. getApplicationMenuFrame() AX corner-pixel hit-test (commit 3)

NSScreen.getApplicationMenuFrame() did AXHelpers.element(at: displayBounds.origin) — a single AX hit-test at the very top-left pixel of the display — and gave up if the result wasn't role .menuBar. That single point is unreliable in three configurations that all apply to the affected machine:

  • Notched Apple silicon Macs. (0, 0) falls outside the screen's rounded-corner mask, so the AX hit-test returns nil.
  • Menu bar accessories that paint near the corner (NotchNook, etc.) can intercept the hit-test and return their own AX element.
  • macOS 26 Liquid Glass menu bar. With the menu bar more transparent, the topmost edge can get classified as the underlying window in some foreground apps.

When the function returned nil, MenuBarItemManager.temporarilyShow silently aborted with "No application menu frame, so not showing …", so clicking a hidden item from the IceBar appeared to do nothing.

Fix: moved the probe into a new AXHelpers.menuBarElement(nearDisplayOrigin:) that hit-tests a few points along the leftmost portion of the menu bar (where the Apple/app menus live, well clear of the notch and any centred accessories) and walks up the AX parent chain when a hit lands inside a specific menu bar item. The probe falls back to the original (0, 0) as the last attempt to preserve the previous behavior on machines where it worked.

Test plan

  • Notched MacBook + secondary display + NotchNook on macOS 26.3.1: IceBar opens reliably, populates with all items, clicks on hidden items show them.
  • Cache no longer enters the invalidation loop at startup (verified via `log show --predicate 'subsystem == "com.jordanbaird.Ice"'`: "Missing control item for hidden section" fires at most once, on the very first cache attempt before status items have registered).
  • "No application menu frame" log message no longer appears.
  • Non-notched single-display Mac (regression check) — please verify in CI / by other reviewers; I don't have the hardware.

Known limitations / follow-ups

The first cache attempt at startup still races against status item registration and emits one "Missing control item for hidden section" warning, but it's now self-recovering on the next attempt instead of looping. A follow-up could retry that first attempt after a short delay; left out of this PR to keep the diff focused.


Researched and authored with help from Claude (Anthropic). Each commit carries a `Co-Authored-By` trailer for transparency.

Misc additional changes and cleanup
`RunLoopLocalEventMonitor` seems to prevent certain buttons from receiving events in macOS 26 Developer Beta 1. This might be a bug in the beta, or `RunLoopLocalEventMonitor` itself. For now, let's just move to a better mouse check implementation that doesn't use continuous event monitoring.

Note the `FIXME` (line 1057). The previous implementation had this problem too, but it was never documented.
A big part of this is hopefully a temporary measure. We need an accurate identifier for every item, and the old way just isn't cutting it right now. Items are all owned by the Control Center in macOS 26, and try as I might, I couldn't find a great way to get the _actual_ host apps for the items.

Must dig deeper into the mines...

Oh yeah, there's also a bunch of random stuff here too that I don't really want to explain. Just know that it probably all fixed something.
This is what I get for trusting Xcode to update my build settings
- Refactor screen capture
- Rework menu bar item getters
- Update immovable/non-hideable info lists
- Remove OSLog wrapper
- Minor migration rework
- Remove old entitlements file
Should also fix a crash when accessing `ControlItem.windowNumber`.
- Rework app lifecycle
- Rework how windows are initialized
- Update documentation comments
- Refactoring and cleanup
This should fix some performance issues that occur during mouse tracking operations (e.g. highlighting a button on hover).
We also store the status item and layout constraint in a separate storage class. Not sure I like this, but the idea is to convey the tightly coupled relationship between the constraint and status item, and to ensure that they are initialized (and deinitialized) at the same time.
Relying on the default behavior seemed to work fine, but is now broken in the macOS 26 Developer Beta. Probably better to handle it explicitly, even if it is just a beta bug.
jordanbaird and others added 22 commits September 2, 2025 13:13
- Documentation changes
- Misc event handling improvements
- Fix temporarily shown item interface check
- Fix broken on screen item check
- Additional minor refactoring
On macOS 26, menu bar items whose owning app does not expose its extras
menu bar via Accessibility can never be resolved to a source PID. The
previous behavior set shouldClearCachedItemWindowIDs on every nil
sourcePID, creating a feedback loop: every cache refresh invalidated
itself, triggering another full re-cache with a fresh XPC roundtrip
plus an O(running apps) AX enumeration for each unresolvable item.

Under this thrash, IceBarPanel.show() would race with the background
re-cache, and toggle()'s isHidden check against controlItem.state could
end up calling hide() on a bar that had just been shown, manifesting
as clicks failing to open the IceBar about half the time.

Fixes:
- SourcePIDCache now records failed lookups with a 30s TTL, skipping
  the expensive AX scan on retry.
- MenuBarItemManager no longer invalidates the item window ID cache
  when sourcePID is nil; items with stable UUID-based tags stay in
  the cache until a running-apps change gives them another chance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…enting

In macOS 26, Control Center re-parents Ice's control item windows, so
their kCGWindowOwnerPID is Control Center, not Ice. The XPC source-PID
lookup then fails for our own items (Ice as an LSUIElement app does not
expose its NSStatusItems via AXExtrasMenuBar), and they fall through to
a synthetic UUID namespace.

The window title is still set correctly ("Ice.ControlItem.Hidden" etc.)
but the namespace mismatch means the resulting tag no longer equals
MenuBarItemTag.hiddenControlItem. ControlItemPair.init can't find the
hidden control item, the entire menu bar item cache is cleared, and the
rebuild loops indefinitely - the IceBar never leaves "Loading menu bar
items...".

Recognise our own control items by their unique title prefix
("Ice.ControlItem.") and force the namespace to .ice regardless of what
the source-PID lookup returned. As a related fallback, items whose owner
is *not* Control Center (i.e. not reparented) now get their owner's
bundle identifier instead of a UUID - useful for any well-behaved status
item app whose AXExtrasMenuBar lookup happens to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NSScreen.getApplicationMenuFrame() hit-tested AX at the single corner
pixel (displayBounds.origin) and gave up if the result wasn't role
.menuBar. That single point is unreliable in three configurations:

  - Notched Apple silicon Macs: (0, 0) is outside the screen's
    rounded-corner mask, so the AX hit-test returns nil.
  - Menu bar accessories that paint near the corner (e.g. NotchNook)
    can intercept the hit-test and return their own AX element.
  - macOS 26 Liquid Glass menu bar: the topmost edge can be classified
    as the underlying window in some foreground apps.

When the lookup returned nil, MenuBarItemManager.temporarilyShow
silently aborted with "No application menu frame, so not showing ...",
so clicking a hidden item from the IceBar appeared to do nothing.

Probe a few inset points along the leftmost portion of the menu bar
(where the Apple/app menus live, well clear of the notch and any
centred accessories) and walk the AX parent chain when a hit lands
inside a menu bar item. The original corner-pixel probe is kept as the
last fallback, to preserve the previous behavior on machines where it
worked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tducp015

tducp015 commented Apr 7, 2026

Copy link
Copy Markdown
Author

Hello, thank you for your work. I have like a million items in my menu bar, and thankfully I have this haha. I've noticed this issue for a while, but didn't have the time to check over it. Hopefully this helps someone else as well!

@darknesscho

Copy link
Copy Markdown

大佬你编译一版然后发布吧,ice 原作者应该已经放弃维护这个项目了。

@tducp015

tducp015 commented Apr 7, 2026

Copy link
Copy Markdown
Author

大佬你编译一版然后发布吧,ice 原作者应该已经放弃维护这个项目了。

Yes, there are a few more bugs there are to fix. I'll probably attach a build here later if anybody needs when I'm free.

It seems features such as hover to open, and the gradient bar is broken. Hover crashes the program for me, and the gradient is rendered in the middle of my screen.

@fleytman

fleytman commented Apr 9, 2026

Copy link
Copy Markdown

@darknesscho

大佬你编译一版然后发布吧,ice 原作者应该已经放弃维护这个项目了。

pls read #867 (comment)

@suddenBook

Copy link
Copy Markdown

大佬你编译一版然后发布吧,ice 原作者应该已经放弃维护这个项目了。

https://github.com/suddenBook/Ice/releases

emindeniz99 pushed a commit to emindeniz99/Ice that referenced this pull request Apr 18, 2026
Ice's macos-26 branch left several critical issues that prevent the app
from working correctly on Tahoe, especially 26.3+ / 26.4+. This change
folds in the fixes from the three community PRs that address them (jordanbaird#903,
jordanbaird#911, jordanbaird#922) and the cache-thrash guard from jordanbaird#874.

Changes:

* `Bridging.getActiveMenuBarDisplayID()` falls back to `CGMainDisplayID()`
  when `CGSCopyActiveMenuBarDisplayIdentifier` returns nil, which is now
  the case on macOS 26.4.1. Without the fallback, the item cache's
  `displayID` stayed `nil` and the layout preview rendered as "Unable to
  display menu bar items" even though items existed.

* `MenuBarItemTag.Namespace` on macOS 26 recognizes Ice's own control
  items by window title ("Ice.ControlItem.*") and falls back to the
  owning application's identifiers when the owner isn't Control Center.
  This stops the UUID-namespace feedback loop that left the layout stuck
  on "Loading menu bar items..." after Control Center re-parented Ice's
  status items.

* `SourcePIDCache` caches failed AX lookups for 30 seconds, and the item
  manager no longer invalidates the window-ID cache when `sourcePID` is
  nil. Those two together stopped the thrash where each failed lookup
  triggered another full scan, racing with `IceBarPanel.show()` and
  causing clicks to drop roughly half the time on notched Macs.

* `AXHelpers.menuBarElement(nearDisplayOrigin:)` probes several inset
  points along the leftmost menu bar region instead of hit-testing the
  exact display corner. Single-point probing fails on notched displays
  (outside the rounded-corner mask), next to menu bar accessories such
  as NotchNook, and on Tahoe's translucent menu bar.
  `getApplicationMenuFrame()` and `hasValidMenuBar(in:for:)` both use
  the new helper.

* `MenuBarItemImageCache.compositeCapture` falls back to `item.bounds`
  when `CGSGetScreenRectForWindow` fails and tolerates a one-pixel
  discrepancy in the composite width. This keeps items that Control
  Center has re-parented from being dropped entirely.

* XPC `.isFromSameTeam()` requirement is only applied when the current
  process actually has a team identifier. Ad-hoc signed builds (the
  default when no signing team is configured) do not, and the old
  requirement refused every peer, leaving the `MenuBarItemService`
  unusable. A new `CodeSignInfo` helper inspects the process's code
  signature.

* The settings detail pane is keyed by the current navigation
  identifier on macOS 26 so that `NavigationSplitView` reliably updates
  on the first sidebar click.
emindeniz99 pushed a commit to emindeniz99/Ice that referenced this pull request Apr 23, 2026
I could only read the descriptions of the community PRs before; after
fetching the actual .patch files I realised my implementation had
real gaps. Fill them in:

* AXHelpers.menuBarElement walks the AX parent chain up to 4 hops
  when a probe point lands on a menu-bar item instead of the menu
  bar itself. Without this, hits on "File"/"Edit"/etc. were dropped
  as not-a-menu-bar. (PR jordanbaird#911)

* MenuBarItem / MenuBarItemTag / Namespace accept an optional
  titleOverride. On some macOS 26 builds Control Center strips the
  titles off reparented status item windows entirely, so the title
  prefix check I added earlier doesn't match anything. The caller
  now frame-matches against live NSStatusItem windows and passes
  the correct "Ice.ControlItem.*" back in. MenuBarItemManager.cacheItemsRegardless
  builds that map by converting the ControlItem window frames from
  Cocoa to CG screen coordinates and looking them up in the current
  CGWindowList. Items without a precomputed identifier fall through
  to the existing title-prefix / owner-bundle / UUID chain. (PR jordanbaird#903)

* Permission now carries a list of settingsURLs and a
  mayRequireRelaunch flag. ScreenRecordingPermission gets three
  URLs — the new macOS 26 PrivacySecurity extension URL with and
  without the ?Privacy_ScreenCapture anchor, plus the legacy
  com.apple.preference.security URL. openSettingsPane launches
  System Settings first (so the URL isn't ignored when it's cold),
  then walks the URL list via NSWorkspace, and finally shells out
  to /usr/bin/open. (PR jordanbaird#928)

* AppState.relaunch reopens the current bundle via
  NSWorkspace.openApplication and terminates the current process.
  PermissionsView, AdvancedSettingsPane, and the Menu Bar Layout
  pane expose a "Relaunch Ice" button and explanatory copy when a
  permission has mayRequireRelaunch == true. (PR jordanbaird#928)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants