Notification surface consolidation + icon & keyboard-focus fixes [4/4] - #1506
Open
theboringhumane wants to merge 13 commits into
Open
Notification surface consolidation + icon & keyboard-focus fixes [4/4]#1506theboringhumane wants to merge 13 commits into
theboringhumane wants to merge 13 commits into
Conversation
3 tasks
theboringhumane
force-pushed
the
stack/04-notification-fixes
branch
from
August 29, 2026 17:12
d3193c9 to
ed32dd3
Compare
theboringhumane
force-pushed
the
stack/04-notification-fixes
branch
from
August 30, 2026 06:09
bc4906d to
affdd12
Compare
2 tasks
…elivery
- Consolidate notification surfaces into one flow: banner -> chin
pill -> expanded card, with an unconditional hold in show() so a
superseded banner never parks off-screen unreleased
- Widen the OTP chin width to clear the hardware notch
- Cap AXI watcher memory pressure (autoreleasepool per scan) and
reinstate a bounded 0.5s idle cadence
- Reply delivery: per-stage timeouts with banner-first ordering,
and a send-failure error state ("Message couldn't be sent. Your
draft is still here - try again or open Messages.")
- Remove the reply-focused auto-focus hijack from the notification
live activity
- Structured logging for notification filtering decisions
- Add a duration parameter to the sneak-peek UI event
Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
The XPC helper's one-shot running-apps match could leave bundleID nil forever (renamed app, helper process owning the banner, an app that quit between posting and capture), so every notification surface fell back to a grey bell instead of the app icon. Resolution now falls back to a direct /Applications probe and a bounded name scan (memoized, hits and misses), and SystemNotificationManager re-resolves app-side in add() when the helper's bundleID is missing. A helper-provided bundleID always wins. Adds BundleIDResolverTests (8 tests: normalization parity, direct probe, directory scan, memoization) and ExpandedViewPixelTests, a render regression guard for the expanded notification card. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
The helper keeps held banners alive by re-performing their details toggle every 2.5s; the toggle expands the banner, and the expanded reply field seizes keyboard focus even parked off-screen. It fired on every arrival regardless of notch state. - Feed the helper the effective notch-open state (refcounted across screens, re-announced after XPC reconnect) via a new fire-and-forget setNotchOpen message - Skip the keep-alive expand while the notch is closed; banners then dismiss naturally (reply requires opening the notch) - Best-effort collapse of expanded held banners on open->closed so focus releases immediately - Parking held banners off-screen continues unconditionally Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
…s/xcstrings reordering) Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
The boringNotchTests target inherited the macOS default ad-hoc identity,
so whenever the app target is signed with the real team cert the test
bundle's Team ID differs from its TEST_HOST and dlopen refuses to load it
('different Team IDs') before any test runs — every test invocation needed
command-line overrides CODE_SIGN_IDENTITY/DEVELOPMENT_TEAM to pass.
Give the test target the app target's ca4c328 choice explicitly:
CODE_SIGN_IDENTITY = Apple Development, DEVELOPMENT_TEAM = JPWMG84CH8
(automatic style). Bare 'xcodebuild test' now loads the bundle and runs
green with no overrides; CI can still override at build time.
Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
add() holds every notification at arrival — before the enqueue branch — and every path into show() (add, promoteNextQueued, cycleToNextQueued) passes one of those already-held notifications. The second hold in show() was a no-op against the helper's held set and an extra XPC round-trip per banner. The add() hold stays: it covers the enqueue path too. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
8cbb4fc accepted Xcode's project normalization, which silently re-cleared the app target's DEVELOPMENT_TEAM and re-added the sdk-qualified ad-hoc identity that ca4c328 had removed. Restore the team on the app target (Debug + Release); the helper target's long-standing ad-hoc config is unchanged. App and test targets now both sign JPWMG84CH8, so bare xcodebuild build/test works with real identities. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
Review of the XPCHelperClient merge found the refcount could leak: cleanupWindows() (screen lock with showOnLockScreen off, or the all-displays toggle) tears windows down without closing the view model, so an open notch never fires its open->closed onChange and the helper's focus gate would stay stuck open forever — silently re-enabling the keyboard-focus steal the gate exists to prevent. - ContentView gains a state-guarded .onDisappear that decrements when a view dies while its notch is open (balanced against the onChange path; no double-decrement since a prior close clears the state first) - ensureRemoteService now always re-announces the effective gate state on a fresh connection instead of only re-sending true, so helper state converges to ours after helper restarts or app-quit-while-open Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
…ze lunarListener 1. Remove dead private getRemoteService() — zero callers repo-wide. 2. Remove vestigial write-only hasLunarListener (property + all writes); the needsListener parameter existed only to feed it, so it goes too. 3. Make lastError truthful: every catch and former try? site now records .transport(underlying:) — MainActor sites set it directly, nonisolated sites hop via MainActor.run. Header comment rewritten to describe the actual behavior. No control flow or return values changed. 4. Remove zero-call-site APIs at all three layers: dismissNotification, isScreenBrightnessAvailable, isKeyboardBrightnessAvailable from client, protocol, and helper @objc wrappers (plus the now-unused KeyboardBrightnessClient.isAvailable). NotificationWatcher.dismiss stays — shared watcher internals are out of this pass's scope. 5. Synchronize NotificationXPCDelegate.lunarListener with an NSLock- guarded backing property: written on MainActor, read on the XPC delivery queue; listener still invoked on the delivery queue. 6. Fix helperAvailable transient: new connectionInterrupted flag set by interruption/invalidation hops, cleared when a fresh connection is stored; existing-connection path reports helperAvailable = !connectionInterrupted instead of unconditional true. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
Audit of the hygiene commit found the connectionInterrupted flag was observably inert: the interruption hop nils remoteService in the same MainActor task that sets the flag, so the existing-connection path can never read it true. Remove the dead logic instead of layering more state on a one-runloop transient. The same review surfaced a real pre-existing bug: the interruption and invalidation handlers nilled connection/remoteService with no identity check, so a stale handler from a deallocated connection could wipe a freshly-built one. Both handlers now capture their connection weakly and bail unless it is still the current one. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
Reply into the originating conversation instead of the first global participant match: scan chats by their participants' display names, prefer a 1:1 chat (preserves account/service/handle/thread), then a matching group chat, and only fall back to a bare participant when exactly one matches library-wide. Zero or 2+ participant matches now return distinct notfound/ambiguous statuses and map to false, so a wrong-person/wrong-thread/wrong-transport send can never report ok. Addresses Alexander5015's review on PR #1503. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
…f main queue hold() inserted into held unconditionally but moved Notification Center's shared window off-screen unconditionally too, hiding unrelated banners and never restoring the position (RC1). The park is now gated on notchOpen like the keep-alive toggle, the window's origin is recorded on first park keyed by CFHash(window), and the position is restored when the last parked hold on that window ends (release, banner-gone, stop). reply() drove the banner's async AX hierarchy with a fixed 400ms Thread.sleep on the helper's main queue — stalling the banner poll and hold refresh — and equated AX action success with delivery (RC4). The reply path now runs on a serial replyQueue (the XPC wrapper completes from there), waits bounded 50ms slices for the reply field to appear (~1.2s cap), and after the send verifies the banner accepted the reply by polling for the field to clear or disappear (~1s cap), reporting false otherwise. AX still cannot verify network delivery; documented. Co-authored-by: TheBoringMajdoor <themajdoor@theboring.name>
theboringhumane
force-pushed
the
stack/04-notification-fixes
branch
from
August 30, 2026 07:24
affdd12 to
3e8ddcb
Compare
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 4 of 4. Based on #1505. Split out of #1496 for reviewability.
Summary
durationevent param/Applicationsprobe + bounded name scan (memoized) in the helper, plus app-side re-resolution inSystemNotificationManager.add(). AddsBundleIDResolverTests(8) +ExpandedViewPixelTestssetNotchOpenXPC feed, collapse on close). Hardened after review: refcount balanced on window teardown (screen lock / display-set change), gate state always re-announced on XPC reconnectholdSystemBannerinshow()(every path in was already held at arrival)JPWMG84CH8so barexcodebuild testloads the bundle; app-targetDEVELOPMENT_TEAMrestored after the Xcode churn commit had re-cleared it (reapplyingca4c328's intent)lastErrorwired at every failure site,lunarListenerlock-synchronized, stale-invalidation identity guardNotes for reviewers
audit-remediation(the original 69-commit branch) was deleted after the split; the closed Notification live activity, compact mode & audit remediation (phases 0-4) #1496 retains its refs.XPCHelperClient.swiftwas rebased over dev's @mainactor isolation (Isolate XPCHelperClient to the main actor #1495); the merged file was independently reviewed (actor consistency + all call sites + client↔protocol↔helper conformance) and the defects found are fixed here.Test plan
xcodebuild -scheme boringNotch buildat every commit in this PR — cleanNotchUIEventTests,PlaybackStateTests,ExpandedViewPixelTests,BundleIDResolverTests(8)