Skip to content

fix(ui): prevent native menu deadlock - #769

Open
beruro wants to merge 4 commits into
developfrom
junyu/fix-native-menu-deadlock
Open

fix(ui): prevent native menu deadlock#769
beruro wants to merge 4 commits into
developfrom
junyu/fix-native-menu-deadlock

Conversation

@beruro

@beruro beruro commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

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 popupNativeMenu lifecycle owner per WebView. It synchronously claims a non-queueing gate before the first menu IPC, builds fresh plain item options, performs exactly one Menu.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.new call 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

  • A rapid second context-menu request is intentionally ignored while the first native menu is active. This avoids stale queued UI but means the user must dismiss the first menu before opening another.
  • Menu actions are now supplied as plain Tauri item options and registered by the single Menu.new call. 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.
  • Coordination remains scoped to one WebView, matching the implicated Tauri resource table; separate WebViews remain isolated.
  • There are no dependency, persistence, schema, public IPC, or wire-format changes. Rollback is a straight revert of the two commits in this PR.

Audit

  • Architecture: all 10 layers reviewed. Ownership, public types, the idle/building/popup/closing state machine, HMR initialization parity, all 14 call sites, naming, cleanup, and failure compatibility are covered. Serialization, persistence, resolver, and backend wire layers are intentionally unchanged.
  • Frontend UI: the skill's single-bug-fix exclusion applies; no JSX, styling, design-system usage, labels, or accessibility structure changed, so no visual audit report was generated.
  • Effects: no Effect was added. Existing mount Effects that synchronize with the external Tauri menu lifecycle now delegate resource cleanup to popupNativeMenu; their existing prop-ref cleanup remains intact.
  • UI evidence: screenshots are not useful because rendered JSX and normal single-menu appearance are unchanged.
Performance area Verdict Evidence Change or reason kept Verification
Background work keep No polling, listener, retry loop, timer, or queue added Busy requests return immediately and are never replayed Concurrent-request unit test
Memory fix One fixed-size { active } slot plus one native menu handle Menu handle closes after success and popup failure; gate releases after build/create/popup/close failure Cleanup and recovery unit tests
Scope/isolation keep HMR-stable symbol is scoped to the current WebView global Separate WebViews do not share state HMR continuity unit test
Rendering/hot path fix Call sites previously created each item via separate IPC Fresh plain options are sent in one Menu.new call Menu creation contract and source sweep

Performance 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.
  • Commit hook — passed lint-staged, scoped TypeScript validation, and staged-file cleanliness checks.
  • git diff --check — passed.
  • PR CI: Rust (clippy) and Check AI attribution passed. Frontend (typecheck · lint · test) is currently blocked by an unrelated error introduced on the latest develop: src/components/PropertyField/PropertyFieldEditable.test.ts:54 omits the required children prop when calling createElement(SearchableDropdown, ...). This PR does not modify that file; the failure must be fixed on the target branch or in a separate single-responsibility PR.
  • The target branch does not yet contain the repository-local verify:quick and verify:final scripts present in the original working tree. The earlier pnpm verify:quick -- ... attempt returned Command verify:quick not found, so the equivalent targeted lint/tests and available full pnpm typecheck command were run directly.
  • Not run: manual packaged macOS reproduction, because it would require force-restarting the user's existing frozen ORG2 process.

@beruro
beruro requested a review from Harry19081 August 10, 2026 15:47
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.

2 participants