Skip to content

Fix menu bar item identification and navigation on macOS Tahoe#903

Open
tabossert wants to merge 5 commits into
jordanbaird:macos-26from
tabossert:fix/tahoe-adhoc-control-item-matching
Open

Fix menu bar item identification and navigation on macOS Tahoe#903
tabossert wants to merge 5 commits into
jordanbaird:macos-26from
tabossert:fix/tahoe-adhoc-control-item-matching

Conversation

@tabossert
Copy link
Copy Markdown

Description

On macOS Tahoe, all menu bar items appear owned by Control Center with nil window titles. This breaks Ice's ability to identify its own control items, causing the menu bar item cache to never populate ("Loading menu bar items..." indefinitely). Additionally, NavigationSplitView in the settings window sometimes fails to update the detail pane on single click.

Changes

  • Added frame-based matching to identify Ice's control items on Tahoe. NSWindow frames are converted from Cocoa coordinates (bottom-left origin) to CG screen coordinates (top-left origin) and matched against CGWindowList bounds to map each control item to its CGWindowID.
  • Added titleOverride parameter to MenuBarItem and MenuBarItemTag init so control items can have their identifier restored as the title when the system returns nil.
  • Added controlItemMap parameter to getMenuBarItems to pass the window ID → identifier mapping through the enumeration pipeline. Items in the map get Ice's PID as sourcePID; all others still resolve via XPC.
  • Added getMenuBarItems(windows:controlItemMap:) overload to accept pre-fetched windows, avoiding a duplicate getMenuBarItemWindows call in cacheItemsRegardless.
  • Added .id() modifier to NavigationSplitView detail pane, keyed to the navigation identifier, to force re-rendering on selection change.

tabossert and others added 2 commits March 21, 2026 13:00
On macOS Tahoe (26), all menu bar items appear owned by Control Center
and have nil window titles, breaking Ice's control item identification.
Additionally, the XPC service fails to connect with ad-hoc code signing
because isFromSameTeam() requires a valid team ID.

This commit bypasses the XPC service and instead identifies control items
by matching NSStatusItem window frames against CGWindowList entries.
NSWindow.windowNumber returns 64-bit values on Tahoe that exceed
CGWindowID's UInt32 range, so direct ID matching isn't possible.
Frame-based matching (x-position and width) reliably maps between the
two ID spaces.

Key changes:
- Disable XPC service startup (blocked by ad-hoc signing)
- Remove isFromSameTeam() peer requirement from XPC session
- Add titleOverride to MenuBarItem/MenuBarItemTag init for Tahoe
- Match control items by frame position instead of PID or window ID
- Pass control item map through getMenuBarItems for correct tagging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .id() modifier keyed to the navigation identifier to force SwiftUI
to re-render the detail pane when the sidebar selection changes. Without
this, NavigationSplitView on macOS Tahoe sometimes fails to update the
detail view on single click while double click always works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tabossert
Copy link
Copy Markdown
Author

Tested on Fresh upgrade to MacOS Tahoe 26.3

@tien0246
Copy link
Copy Markdown

tien0246 commented Mar 29, 2026

Good pr, thank you

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)
trevor and others added 3 commits May 15, 2026 12:04
On macOS Tahoe (26), `XPCListener(service:, requirement: .isFromSameTeam())`
silently drops every incoming check-in when the service is ad-hoc signed
(no team identifier to compare against). The listener returns successfully
from construction, so the existing try/catch fallback to an unrestricted
listener never triggers — clients see `XPCRichError code=1` on every
sendSync, and the service logs `Dropping check-in message due to code
signing requirement` / `Bogus check-in attempt. Ignoring.`

Gate the same-team requirement behind a runtime team-ID check via the new
shared `currentProcessTeamID()` helper (using SecCodeCopySigningInformation):
properly signed builds keep the requirement; ad-hoc builds fall through to
the unrestricted listener so the service actually serves requests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The cancellation handler captured `[weak self]` and set `self.session = nil`
to mark the session for recreation. That assignment ran on the session's
target queue, outside the storage lock that protects every other access to
`self.session`. When a request was in flight on one thread and the session
was being cancelled on another, the unlocked nil-assignment released the
XPCSession concurrently with `sendSync`'s `_os_object_retain`, tripping
`libdispatch: API MISUSE: Resurrection of an object` and crashing the app.

Move the responsibility for nilling out the session into `send`'s catch
block, which already runs under `storage.withLock`. The cancellation
handler now just logs. Subsequent `getOrCreateSession` calls see the stale
reference, `sendSync` fails, and the catch block clears it for the next
attempt — all lock-protected.

Also gate `setPeerRequirement(.isFromSameTeam())` on `currentProcessTeamID()`
matching the listener side, so the client doesn't reject responses from an
ad-hoc-signed service.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .id() fix on the detail pane only helps when the List's selection
binding actually propagates. On macOS Tahoe, NavigationSplitView's List
selection sometimes updates the highlight on single click without writing
to the bound value (a second click is required to commit it), so the
detail pane and navigation title stay stale while the sidebar visually
shows the new selection.

Attach a `simultaneousGesture(TapGesture)` to each sidebar item that
writes the identifier into navigationState directly, alongside the List's
own selection handling. The click always commits the selection in one
shot, and the existing .id() then forces the detail pane to re-render.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pdurlej added a commit to pdurlej/Ice that referenced this pull request May 24, 2026
Upstream PR jordanbaird#903 introduced MenuBarItemService.xpc with this line:

    listener = try XPCListener(service: name,
                               requirement: .isFromSameTeam()) { ... }

`.isFromSameTeam()` requires the listener and every peer to be signed
with the same Apple Team Identifier. That works for builds Jordan
produces from his Developer ID Application certificate. It does NOT
work for any ad-hoc-signed build (TeamIdentifier = empty), because
empty-vs-empty is never treated as a match — the listener rejects
every check-in attempt with "Bogus check-in attempt. Ignoring." and
"Dropping check-in message due to code signing requirement".

The visible symptom is the Menu Bar Layout settings pane spinning
forever on "Loading menu bar items…" — XPC never returns, so Ice
never gets the cached item snapshots. This is the same class of bug
reported in upstream issues jordanbaird#744 (46 reactions) and jordanbaird#891 (30 react-
ions), and it bites every community fork that ships without an Apple
Developer Program account — which is roughly every community fork.

This commit reads the running process's actual Team Identifier via
SecCodeCopySigningInformation; if it's nil (ad-hoc / unsigned), we
fall through to the no-requirement activation path. If it's set
(properly Developer-ID-signed build), behaviour is unchanged — the
strict same-team requirement still applies.

For our fire fork:
  - 0.11.13-fire.0..fire.1 (ad-hoc, this branch's defaults) → fixed.
  - 0.11.13-fire.2+ once signed with our Developer ID → unchanged,
    still uses .isFromSameTeam() because we'll have a team ID.

Also bumps MARKETING_VERSION 0.11.13-fire.1 → 0.11.13-fire.2 and
CURRENT_PROJECT_VERSION 1123 → 1124 so Sparkle in installed fire.1
recognizes this as a newer build and offers the update.
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.

2 participants