Skip to content

Fix notch indicator crash from SwiftUI animating the panel window inside layout - #1285

Merged
SeoFood merged 4 commits into
TypeWhisper:mainfrom
ebolamerican:fix/notch-panel-animated-window-size-loop
Sep 9, 2026
Merged

Fix notch indicator crash from SwiftUI animating the panel window inside layout#1285
SeoFood merged 4 commits into
TypeWhisper:mainfrom
ebolamerican:fix/notch-panel-animated-window-size-loop

Conversation

@ebolamerican

@ebolamerican ebolamerican commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #1229.

Root cause (captured live with lldb)

The daily NSInternalInconsistencyException abort in _postWindowNeedsUpdateConstraints reported 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 on objc_exception_throw gave the reason text:

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. <TypeWhisper.NotchIndicatorPanel> {{685, -1883}, {1460, 2990}}

The panel had grown from 500×500 to 1460×2990. Mechanism: NSHostingView owns a WindowSizeBridge (SwiftUI's AnimatedRootSizeFeatureDelegate). Whenever the root view's size changes inside an animated transaction, the bridge animates the window frame to follow it — from windowDidLayout, 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 4bc7d613b on macOS 27 with Xcode 26.6:

  • The new geometry regression failed against the previous implementation at three panel sizes. It passes with explicit positioning.
  • Added coverage for fixed hosting/root size, horizontal centering, top anchoring, animated feedback transitions, and safe-area changes.
  • Updated first-mouse coverage to hit-test the actual receiving view through the container hierarchy.
  • Full app suite: 1,760 tests passed, 0 failures. No first-party compiler warnings.
  • Signed development build passed. Manual smoke test passed, confirmed by Marco on 2026-09-09: with the Notch indicator enabled, starting a new recording while feedback was visible (mostly “Abgebrochen” / cancelled, and once “Zu kurz” / too short) kept the content centered and fully visible. No crashes, hangs, or other negative behavior were observed during this short test.

Exact local verification commands (run from the PR checkout):

xcodebuild test -skipPackagePluginValidation -project TypeWhisper.xcodeproj -scheme TypeWhisper -destination 'platform=macOS,arch=arm64' -derivedDataPath /private/tmp/typewhisper-1229-tests -clonedSourcePackagesDirPath /Users/marco/Projects/typewhisper-mac-dev/DerivedData/SourcePackages -parallel-testing-enabled NO CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO > /private/tmp/typewhisper-1229-green.log 2>&1
bash scripts/check_first_party_warnings.sh /private/tmp/typewhisper-1229-green.log

Original investigation and validation:

  • With lldb attached to the built app and breakpoints on NSHostingView.updateAnimatedWindowSize and objc_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) entered updateAnimatedWindowSize zero times with no exception. Before this change the same sequence entered it dozens of times per run and aborted.
  • DictationViewModelIndicatorSettingsTests (23 tests) pass.
  • Several days of daily use with no recurrence of the previously ~daily crash.

The Overlay and Minimal indicator panels carry the same bridge (confirmed via WindowSizeBridge.clampedWindowSize breakpoints 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

  • Bug Fixes
    • Fixed a crash that could occur when resizing the notch panel.
    • Improved notch panel sizing stability to prevent unexpected resize animations and layout-related errors.
    • Improved the positioning of notch panel content during resizing, keeping it consistently aligned and sized.
    • Improved panel interaction behavior so visible panel content correctly receives first-click input.

ebolamerican and others added 2 commits September 5, 2026 20:22
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>
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 1ec5c1a7-f1b1-4d59-afeb-a74e27780ea1

📥 Commits

Reviewing files that changed from the base of the PR and between f3cc53f and 4bc7d61.

📒 Files selected for processing (3)
  • TypeWhisper/Views/NotchIndicatorPanel.swift
  • TypeWhisperTests/DictationViewModelIndicatorSettingsTests.swift
  • TypeWhisperTests/MeetingAutomationCountdownIndicatorTests.swift

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Notch panel layout

Layer / File(s) Summary
Fixed hosting container
TypeWhisper/Views/NotchIndicatorPanel.swift, TypeWhisperTests/DictationViewModelIndicatorSettingsTests.swift
The panel disables safe-area regions and uses NotchHostingContainerView to keep the hosting view size fixed while centering and top-anchoring it. Lifecycle tests validate geometry, root sizing, panel stability, and feedback transitions.
Panel hit-testing validation
TypeWhisperTests/MeetingAutomationCountdownIndicatorTests.swift
Panel tests render Color.black, force layout, hit-test the content center, and verify first-mouse acceptance.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 4bc7d

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: seofood

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation directly addresses issue [#1229] by preventing NSHostingView size changes during window layout, opting out of safe-area regions, and adding regression coverage for the crash mechani…
Out of Scope Changes check ✅ Passed The implementation and test changes are related to fixing issue [#1229] and validating notch panel sizing, alignment, safe-area behavior, and input handling. No unrelated code changes are identified.
Title check ✅ Passed The title clearly identifies the primary change: fixing the notch indicator crash caused by SwiftUI animating the panel window during layout.
Description check ✅ Passed The description provides a detailed root cause, fix, verification results, manual test confirmation, and linked issue. It does not use the required Summary and Test Plan headings or checklist format, …
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

A rabbit checks the notch,
Fixed frames hop through resize,
Safe areas fade,
Tests guard each rendered point,
Crashes lose their trail.

Comment @coderabbitai help to get the list of available commands.

@SeoFood SeoFood left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread TypeWhisper/Views/NotchIndicatorPanel.swift Outdated
Comment thread TypeWhisper/Views/NotchIndicatorPanel.swift Outdated
@shuangwangnyc

Copy link
Copy Markdown

Independent corroboration from another TypeWhisper 1.6.0 (build 1091) installation on macOS 26, plus a published patch and regression test:

shuangwangnyc@cedb741

Of eight crash reports inspected from September 1–7, six were window-layout failures (five shared the NSHostingView.updateAnimatedWindowSize / safe-area invalidation / _postWindowNeedsUpdateConstraints path). The September 7 unified log explicitly identified TypeWhisper.NotchIndicatorPanel with a 1460×2996 frame, instead of its normal 500×500 size. Two other reports were audio-thread crashes and are outside this patch.

We independently arrived at safeAreaRegions = [] plus a plain AppKit container around the hosting view. A safe-area-only attempt still reproduced the AppKit constraint-cycle exception in the focused regression, which supports this PR's observation that safe-area opt-out alone is insufficient.

Our patch is based on the v1.6.0 tag and differs from this PR: it uses .width / .height autoresizing, whereas this PR keeps the hosting view fixed-size with flexible margins. We have not verified that our variant prevents the exact animated toast-then-hotkey sequence described here; the fixed-size approach here has stronger evidence for that scenario. Publishing our work as supporting evidence and a test source, rather than claiming it supersedes this PR.

The added testNotchContentKeepsPanelGeometryWhenSystemSafeAreaChanges injects additional safe-area insets (32, 64, 0) across passive → interactive → passive transitions and checks zero SwiftUI insets, panel geometry, and non-key behavior. It failed with 12 assertions on unmodified v1.6.0; all 109 tests in the indicator test file passed with our patch in a focused local test target. The size assertion assumes our autoresizing variant and would need adaptation for the fixed-size hosting view in this PR.

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=NO

Related: #1229. No raw crash dumps or personal diagnostic data are attached.

@SeoFood

SeoFood commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Manual smoke test confirmed on the signed development build of 4bc7d613b067d13e7a41e50438d7b2ca20d38301 (macOS 27).

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 SeoFood left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@SeoFood
SeoFood merged commit 309eea9 into TypeWhisper:main Sep 9, 2026
11 checks passed
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.

Recurring crash: NSInternalInconsistencyException in NSHostingView constraint update during display cycle

3 participants