fix(ui): prevent native menu deadlock - #769
Open
beruro wants to merge 4 commits into
Open
Conversation
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.
Problem
Opening a second Tauri native context menu while another menu is tracking input can deadlock the desktop app on macOS. The popup command retains the WebView resource-table lock while waiting for the native menu, and a nested menu construction request can wait on that same lock from the main thread. The result is an ORG2 window that macOS reports as not responding.
The first implementation centralized only the single-flight gate. Each UI surface still owned the same menu-item resource creation, menu construction, popup, fallback, busy handling, and cleanup sequence, leaving the correctness boundary easy to bypass and native resources unclosed.
Solution
Introduce one high-level
popupNativeMenulifecycle owner per WebView. It synchronously claims a non-queueing gate before the first menu IPC, builds fresh plain item options, performs exactly oneMenu.new, handles the optional positioned-popup cursor fallback, closes the native menu resource, and releases the gate after success or any failure. If popup and cleanup both fail, both causes are preserved.All 14 production menu surfaces now provide only their labels, conditions, ordering, and actions. They no longer import Tauri's menu API or create/popup resources themselves. Plain item options let Tauri register all actions during the single
Menu.newcall instead of issuing one IPC per item.A source-contract test rejects any future production import of
@tauri-apps/api/menu,Menu.new/MenuItem.new, or.popup()outside the lifecycle owner. The resulting invariant is: at most one native menu lifecycle is active in a WebView; duplicate interactions are dropped before menu construction; every created menu is closed before the gate returns to idle.Potential risks
Menu.newcall. This is supported by the installed Tauri API and covered by type/unit tests, but was not manually exercised in a newly packaged macOS build because the existing frozen process was not force-restarted.menu.close()now runs after popup completion and its failure is surfaced. This fixes resource ownership but can expose a cleanup error that the previous implementation silently leaked.Audit
popupNativeMenu; their existing prop-ref cleanup remains intact.{ active }slot plus one native menu handleMenu.newcallPerformance verdict: pass. Packaged-runtime CPU/RSS measurement was not run; no retained background loop or growing collection was introduced, and native-resource cleanup is verified at the API boundary.
Verification
pnpm exec vitest run src/util/platform/tauri/nativeMenuPopup.test.ts src/features/TaskKanban/utils/cardContextMenu.test.ts src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/channelsSection.menuItems.test.ts src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/channelsSection.orgSwitch.test.ts src/modules/WorkStation/CodeEditor/Panels/EditorPrimarySidebar/content/SourceControlContent/components/SourceControlContextMenu.test.ts— passed, 5 files / 39 tests.pnpm lint:file -- src/components/FileTreeContent/FileExplorerMenu.tsx src/components/WindowChrome/WindowsTopBar.tsx src/engines/ChatPanel/ChatPanelTabContextMenu.tsx src/features/TaskKanban/hooks/useKanbanCardContextMenu.ts src/hooks/ui/useResizeContextMenu.ts src/modules/WorkStation/CodeEditor/Panels/EditorPrimarySidebar/content/GitHistoryContent/GitHistoryContextMenu.tsx src/modules/WorkStation/CodeEditor/Panels/EditorPrimarySidebar/content/SourceControlContent/components/SourceControlContextMenu.tsx src/modules/WorkStation/shared/TabBar/TabContextMenu.tsx src/scaffold/GlobalSpotlight/components/SpotlightItemRow.tsx src/scaffold/NavigationSidebar/SidebarBase.tsx src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/channelsSection.orgSwitch.test.ts src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/channelsSection.tsx src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/cloudSessionsSection.rowItemBuilder.tsx src/scaffold/NavigationSidebar/connectors/WorkstationSidebarConnector/localChannelsSection.tsx src/scaffold/NavigationSidebar/connectors/useWorkstationSidebarContextMenu.ts src/util/platform/tauri/nativeMenuPopup.test.ts src/util/platform/tauri/nativeMenuPopup.ts— passed.pnpm lint:file -- src/util/platform/tauri/nativeMenuPopup.ts— passed after the final compatibility edit.pnpm typecheck— passed; the full TypeScript check executed after the final edit.lint-staged, scoped TypeScript validation, and staged-file cleanliness checks.git diff --check— passed.Rust (clippy)andCheck AI attributionpassed.Frontend (typecheck · lint · test)is currently blocked by an unrelated error introduced on the latestdevelop:src/components/PropertyField/PropertyFieldEditable.test.ts:54omits the requiredchildrenprop when callingcreateElement(SearchableDropdown, ...). This PR does not modify that file; the failure must be fixed on the target branch or in a separate single-responsibility PR.verify:quickandverify:finalscripts present in the original working tree. The earlierpnpm verify:quick -- ...attempt returnedCommand verify:quick not found, so the equivalent targeted lint/tests and available fullpnpm typecheckcommand were run directly.