…cOS 26 layout-cycle crash
Adds in-menu access to org resources after connect, replaces SwiftUI
MenuBarExtra with an AppKit-hosted menu bar shell, and fixes a hard
crash on macOS 26 caused by NavigationSplitView + .windowResizability
inside a SwiftUI WindowGroup.
Resources feature:
- New Models: UserResource, UserSiteResource, GetUserResourcesData,
SiteResourceDetail (with siteIds/siteNames/siteOnlines, port ranges,
ICMP flag), ListAllSiteResourcesData.
- APIClient.listUserResources(orgId:),
APIClient.listAllSiteResources(orgId:pageSize:).
- ResourceCache (@mainactor ObservableObject): 3-min background polling
while connected, manual refresh re-arms the timer, refreshSequence
token guards against concurrent refreshes overwriting each other.
- Menu UX: Public/Private hover submenus with live search, site
grouping for Private list with online indicators, per-row Open /
Copy Alias / Copy Address actions with transient feedback, manual
Refresh row, "Connect to Pangolin" placeholder when disconnected,
detail panel (3rd depth) showing Domain / Destination / Mode /
Alias / TCP / UDP / ICMP.
Menu-bar architecture (SwiftUI -> AppKit):
- MainMenuController: NSStatusItem + custom FocusableMenuPanel
(NSPanel) + FirstMouseHostingController. Connected status icon is
composited (orange disc + white checkmark, cached).
- MenuPanelController: reusable controller for 2-depth submenus and
3-depth detail panels. Anchored NSPanel, key-window transfer on
first click via sendEvent override, full deinit cleanup of click /
mouse-move monitors, hide timer, and the panel itself.
- SubmenuCoordinator: only one HoverSubmenuRow open at a time.
- HoverSubmenuRow: hover-delay scheduling, keep-open signal so a
3-depth detail panel keeps its parent submenu open.
- AnchorReader (NSViewRepresentable): reports row screen frames;
overrides setFrameOrigin/setFrameSize so position-only layout
shifts (e.g. logout shrinking the menu) update the anchor instead
of leaving submenus aligned to pre-logout coordinates.
- CustomSwitch: replaces SwiftUI Toggle(.switch), which dims when its
window isn't key.
WindowGroup -> AppKit (fixes macOS 26 crash):
- AppWindowsController singleton lazily creates NSWindow +
NSHostingController for Login, Onboarding, Preferences. All
window-level configuration (styleMask, identifier, title, button
visibility, content size) is set explicitly at creation, never
mutated during a layout pass.
- centerOnScreen places windows at horizontal center, slightly above
vertical midline of screen.visibleFrame (multi-display friendly).
- NSWindowDelegate.windowWillClose updates dock activation policy.
- App body reduced to Settings { EmptyView() } (Scene placeholder).
- pangolinOpenWindow notification now observed in PangolinAppDelegate
and dispatched to AppWindowsController.show(id:); helper
postOpenWindow(id:) replaces direct openWindow usage in NSPanel-
hosted views.
- Removed window-management code from view bodies (WindowAccessor in
LoginView, OnboardingWindowAccessor, PreferencesWindowAccessor,
configureWindow / hideMenuBarItems / handleWindowAppear etc.). On
macOS 26 those ran during the display-cycle observer and threw
NSException from _postWindowNeedsUpdateConstraints.
- Removed outer .frame(minWidth: 600, minHeight: 400) on
PreferencesWindow — combined with NavigationSplitView and
.windowResizability(.contentSize) it produced the layout cycle.
navigationSplitViewColumnWidth(min:200) still enforces a sane
minimum sidebar width.
Onboarding UX:
- mainContent split: onboardingMenuContent shows minimal menu
(Open Pangolin Setup + Quit) during onboarding; fullMenuContent
shows the post-onboarding menu. Previously the full menu was
visible during setup, which was unintended.
Bug fixes:
- ResourceCache.refresh() concurrent refresh race: stale results
from an older in-flight request can no longer overwrite a newer
one's results.
- HoverSubmenuRow keepOpenSignal race: .onChange(of:keepOpenSignal)
now re-runs scheduleUpdate so a stale close-timer doesn't fire
after detail-panel hover state flips.
- Status-icon animation timer: each queued Task checks the current
tunnelManager.status before applying a loading frame, preventing
a frame queued before transition to .connected from overwriting
the connected-badge icon.
- AuthManager.hasInitialized prevents re-running the full init cycle
on every menu-popover open (eliminated "Loading..." flicker).
Cleanup:
- ~600 lines of dead code removed: legacy NSMenu-based resources,
search window, menu-dropdown row, view modes / popover modes
enums, redundant @State, dead notifications.
- Stale comment ("5-minute interval" -> 3 minutes), unused imports.
- Removed file: Pangolin/macOS/UI/Preferences/PreferencesWindowAccessor.swift.
Notes:
- Functional behavior of VPN, auth, account management, system
extension activation, and IPC is unchanged. TunnelManager.swift,
system-extension request paths, entitlements, bundle IDs, and
the PacketTunnel target are not modified.
- Deployment target stays at macOS 14.0; no APIs above that level
are introduced.
Summary
MenuBarExtrawith an AppKit-hosted menu bar shell (NSStatusItem+NSPanel+NSHostingController) for reliable click handling, key-window transfer, and uniform behavior across all panel depths.WindowGroups with AppKitNSWindow+NSHostingControllermanaged by anAppWindowsControllersingleton. This fixes a hard crash on macOS 26 wherePreferences'sNavigationSplitView+.windowResizability(.contentSize)produced a layout cycle that threwNSExceptionfrom_postWindowNeedsUpdateConstraints.TunnelManager.swift, system-extension request paths, entitlements, bundle IDs, and the PacketTunnel target are not modified.A more detailed write-up is included in
CHANGES.md.What's in the diff
New
ResourceCache(long-lived@MainActor ObservableObject) — 3-min background poll while connected;refreshSequencetoken guards against concurrent refreshes overwriting each other with stale data.APIClient.listUserResources(orgId:),APIClient.listAllSiteResources(orgId:pageSize:).Models.swiftfor resources and site details (incl. TCP/UDP port ranges, ICMP flag, site online state).MainMenuController,MenuPanelController,SubmenuCoordinator,HoverSubmenuRow,AnchorReader,FocusableMenuPanel,FirstMouseHostingController,CustomSwitch,ConnectToggleRow.AppWindowsController— owns Login / Onboarding / PreferencesNSWindows;centerOnScreen(_:)helper for multi-display friendly placement;windowWillCloseupdates dock activation policy.postOpenWindow(id:)notification helper (replaces@Environment(\.openWindow)use in NSPanel-hosted views).Removed
OpenWindowBridgeSwiftUI scene (no longer needed).WindowAccessor(LoginView),OnboardingWindowAccessor,PreferencesWindowAccessor(file deleted).configureWindow(_:)/configureOnboardingWindow(_:)methods that synchronously mutatedstyleMaskduring SwiftUI body / display-cycle observers..frame(minWidth: 600, minHeight: 400)onPreferencesWindow..onReceive(NSWindow.didBecomeKeyNotification)synchronous styleMask mutation in PreferencesWindow / LoginView.setActivationPolicy(.regular)/.accessorytoggling scattered across views (centralized inAppWindowsController).@Environment(\.openWindow)usage in MenuBarView.Bug fixes
ResourceCache.refresh()concurrent-refresh race (token guard).HoverSubmenuRowkeep-open signal race when 3-depth detail panel togglesdetailPopoverHovered(.onChange(of: keepOpenSignal)re-runs scheduling).MenuPanelController.deinitcleans upclickMonitor,mouseMoveMonitor,hideTimer,panel.orderOut(was only releasingclickMonitor).tunnelManager.statusbefore applying a loading frame.AnchorReaderoverridessetFrameOrigin/setFrameSizeso position-only layout shifts (e.g. logout shrinks the menu) update the anchor instead of leaving submenus aligned to pre-logout coordinates.AuthManager.hasInitializedflag prevents re-running the full init cycle on every menu-popover open (eliminated "Loading…" flicker).Cleanup
NSMenu-based resources view, separate Resource Search window, Menu-dropdown row helper, unusedMenuViewMode/ResourcesPopoverModeenums, redundant@State, dead notification subscriptions).import os.log.Test plan
xcodebuildDebug, ad-hoc-signed locally).disconnected→…(animated) → connected badge (orange disc + white checkmark).NSExceptionfrom_postWindowNeedsUpdateConstraints).Refreshrow + 3-min background polling while connected.onboardingViewModel.isPresentingis true.terminate).