Fix notch indicator crash from SwiftUI animating the panel window inside layout - #1285
Conversation
Every crash report on this machine since 8/26 shares one stack: inside the window's own layout pass, NSHostingView.windowDidLayout -> updateAnimatedWindowSize -> setFrame triggers AppKit's _effectiveSafeAreaCornerInsets KVO on the hosting view, which SwiftUI answers with invalidateSafeAreaCornerInsets -> setNeedsUpdateConstraints, and _postWindowNeedsUpdateConstraints raises NSInternalInconsistencyException because layout is already in progress. The exception is rethrown by the display-cycle observer and terminates the process. The notch panel is the only app window that sits over the hardware notch, the one place a notched display carries safe-area corner insets. A standalone probe confirmed that with the default safeAreaRegions AppKit fires _effectiveSafeAreaCornerInsets / _effectiveCornerRadii on the hosting view for every frame change, and that with safeAreaRegions = [] neither key is observed at all. The notch content lays itself out from NotchGeometry and never consumes the safe area, so opting out removes the crashing observer without changing the layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Captured live under lldb: the app aborts with "The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window" on the NotchIndicatorPanel, whose frame had grown to 1460x2990. NSHostingView keeps a WindowSizeBridge that animates the window frame to follow the root view whenever the root's size changes inside an animated SwiftUI transaction, and it does so from windowDidLayout - inside the layout pass. The panel's toast transitions resize the window in the same update as the toast animation, so the root size change is animated, the bridge resizes the window back from inside layout (observed: updateAnimatedWindowSize(500x500) while the panel sat at toast size), that changes the root size again, and the loop exhausts AppKit's constraint-pass limit. sizingOptions = [] does not disable that bridge. The hosting view now stays at the non-interactive panel size inside a plain container; only the window resizes, with flexible margins keeping the hosting view centered and top-anchored. With the root size constant the bridge never has anything to animate. Verified with lldb: across the toast-then-hotkey sequence that previously crashed, updateAnimatedWindowSize is never entered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe notch panel now embeds its hosting view in a fixed-size container. The container moves the hosting view without resizing it. Tests cover geometry, safe-area behavior, feedback transitions, and first-mouse handling. ChangesNotch panel layout
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The notch indicator now resizes its window without resizing the SwiftUI hosting view, preventing the layout feedback loop that caused recurring crashes. No concrete current-head merge-blocking risk remains. Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant NotchIndicatorPanel
participant NotchHostingContainerView
participant NSHostingView
NotchIndicatorPanel->>NotchHostingContainerView: Set fixed initial size
NotchIndicatorPanel->>NSHostingView: Disable safe-area regions
NotchHostingContainerView->>NSHostingView: Move origin during resize
NotchHostingContainerView-->>NSHostingView: Keep frame size unchanged
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
A rabbit checks the notch, Comment |
SeoFood
left a comment
There was a problem hiding this comment.
Two findings need to be addressed before merging.
I reviewed f3cc53feb0ef03a554ef2c6981edd2cc095ce898 and ran a standalone AppKit/SwiftUI probe on macOS 27 using the new hosting/container arrangement, plus a comparison with the previous direct-hosting arrangement. The probe reproduces the horizontal alignment regression described inline. I did not independently reproduce the original crash or run the full app test suite locally.
The existing CI run reports 1 failure out of 1,745 tests, in MeetingAutomationCountdownIndicatorTests.testPanelsRemainNonactivatingAndAcceptFirstMouse. The release build and other executed checks passed; Swift CodeQL was skipped. CodeRabbit reported no actionable findings, and there were no open review threads before this review.
|
Independent corroboration from another TypeWhisper 1.6.0 (build 1091) installation on macOS 26, plus a published patch and regression test: Of eight crash reports inspected from September 1–7, six were window-layout failures (five shared the We independently arrived at Our patch is based on the v1.6.0 tag and differs from this PR: it uses The added Validation limits: Debug and Release app builds passed. The full upstream suite did not complete because of local Metal toolchain/build-environment problems; no full-suite pass is claimed. The patched Release app was installed locally and passed a brief startup/window-geometry check, but long-term recurrence and end-to-end dictation have not yet been verified. Standard command for the lifecycle regression in the upstream project (requires its normal build dependencies; the successful run above used a focused local target): xcodebuild test -project TypeWhisper.xcodeproj -scheme TypeWhisper -destination 'platform=macOS,arch=arm64' -parallel-testing-enabled NO -only-testing:TypeWhisperTests/NotchIndicatorPanelLifecycleTests CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NORelated: #1229. No raw crash dumps or personal diagnostic data are attached. |
|
Manual smoke test confirmed on the signed development build of With the Notch indicator enabled, I tested starting a new recording while a feedback toast was visible. Most attempts displayed “Abgebrochen” (cancelled); one displayed “Zu kurz” (too short). The content stayed centered and fully visible, with no crashes, hangs, or other negative behavior noticed. This confirms the short manual feedback-to-recording test. Long-term recurrence of the original intermittent crash has not yet been evaluated on this build. |
SeoFood
left a comment
There was a problem hiding this comment.
Re-reviewed 4bc7d61. Both previously requested changes are addressed and their threads are resolved. The regression tests cover fixed hosting size, centering, top anchoring, animated feedback transitions, safe-area handling, and first-mouse hit testing. All 1,760 local app tests passed, and the manual feedback-to-recording smoke test passed. CodeRabbit reported no actionable findings on this head. No remaining code-review blockers.
Closes #1229.
Root cause (captured live with lldb)
The daily
NSInternalInconsistencyExceptionabort in_postWindowNeedsUpdateConstraintsreported in #1229 is a feedback loop between the notch panel's own toast-time resize and SwiftUI's window-size bridge. Attaching lldb to the running app and breaking onobjc_exception_throwgave the reason text:The panel had grown from 500×500 to 1460×2990. Mechanism:
NSHostingViewowns aWindowSizeBridge(SwiftUI'sAnimatedRootSizeFeatureDelegate). Whenever the root view's size changes inside an animated transaction, the bridge animates the window frame to follow it — fromwindowDidLayout, i.e. inside the layout pass (breakpoint:updateAnimatedWindowSize(500×500)fired while the panel sat at toast size). The panel's toast transitions resize the window in the same update as the toast's.animation(value:), so the root size change is animated, the bridge resizes the window back from inside layout, that changes the root size again, and the loop exhausts AppKit's constraint-pass limit.sizingOptions = []does not disable that bridge.Fix
The hosting view no longer changes size: it stays at the non-interactive panel size inside an AppKit container
contentView, and only the window resizes. The container explicitly updates the hosting view origin to keep it centered and top-anchored; flexible margins alone leave the toast shifted and clipped when shrinking from 500 points. With the root size constant the bridge never has anything to animate.The first commit opts the panel out of safe-area regions (it never consumes the safe area); it removes one observer but is not the fix on its own.
Verification
Maintainer validation of
4bc7d613bon macOS 27 with Xcode 26.6:Exact local verification commands (run from the PR checkout):
Original investigation and validation:
NSHostingView.updateAnimatedWindowSizeandobjc_exception_throw: five runs of the previously crashing sequence (a feedback toast up, then the dictation hotkey pressed into it so the panel goes small → large mid-animation) enteredupdateAnimatedWindowSizezero times with no exception. Before this change the same sequence entered it dozens of times per run and aborted.DictationViewModelIndicatorSettingsTests(23 tests) pass.The Overlay and Minimal indicator panels carry the same bridge (confirmed via
WindowSizeBridge.clampedWindowSizebreakpoints on all three windows); this PR changes only the notch panel, which is the one that reproduces and could be verified.🤖 Generated with Claude Code
Summary by CodeRabbit