From 18a4f4aed358246a597b457e3a45abfe052055e5 Mon Sep 17 00:00:00 2001 From: Andrew Mikofalvy <5668128+amikofalvy@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:21:20 -0700 Subject: [PATCH] fix(open-knowledge): Open-with-AI menu non-modal so it can't trap the UI (#1620) * fix(open-knowledge): Open-with-AI menu non-modal so it can't trap the UI The editor toolbar's Open-with-AI dropdown is a modal Radix menu. After #1604 made it openable on the macOS desktop host, opening it set body { pointer-events: none } over the rest of the chrome. The trigger lives in the title-bar -webkit-app-region: drag zone, where the outside-pointerdown a modal relies on for dismissal does not reliably reach Radix, so the menu could only be closed by selecting an agent, and the bottom-left project switcher (and the rest of the UI) was frozen behind it until then. Render the menu non-modal: opening it no longer disables outside pointer events, and the existing data-electron-drag no-drag rule handles outside-click dismissal. Keep #1604's onClick-to-open path (still needed to open inside the drag region). Adds a jsdom test pinning the non-modal contract (opening must not set body pointer-events to none). * test(open-knowledge): mock useHandoffDispatch explicitly in non-modal dom test Addresses PR review: mock the dispatch hook directly instead of relying on it resolving useConfigContext through the config-context mock, matching sibling dom tests and keeping the test meaningful if the hook's imports change. GitOrigin-RevId: 8ea4cecf00dd38954431e431fed7a9580a7b3afe --- .changeset/fix-open-with-ai-modal-trap.md | 9 +++ .../OpenInAgentMenu.non-modal.dom.test.tsx | 57 +++++++++++++++++++ .../components/handoff/OpenInAgentMenu.tsx | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-open-with-ai-modal-trap.md create mode 100644 packages/app/src/components/handoff/OpenInAgentMenu.non-modal.dom.test.tsx diff --git a/.changeset/fix-open-with-ai-modal-trap.md b/.changeset/fix-open-with-ai-modal-trap.md new file mode 100644 index 00000000..6a33c0c6 --- /dev/null +++ b/.changeset/fix-open-with-ai-modal-trap.md @@ -0,0 +1,9 @@ +--- +"@inkeep/open-knowledge": patch +"@inkeep/open-knowledge-core": patch +"@inkeep/open-knowledge-server": patch +"@inkeep/open-knowledge-app": patch +"@inkeep/open-knowledge-desktop": patch +--- + +Fix the editor toolbar's "Open with AI" menu freezing the rest of the app in the macOS desktop app. Once the menu became openable on the desktop host, its default modal behavior disabled pointer events on everything outside the menu while it was open. Because the menu lives in the macOS title-bar drag region — where the outside-click that normally dismisses a modal doesn't reliably reach the menu — the only way to close it was to pick an agent, and meanwhile the rest of the chrome (notably the bottom-left project switcher) couldn't be clicked. The menu is now non-modal: opening it no longer blocks the rest of the UI, and clicking anywhere outside dismisses it. Browsers (`ok ui`) are unaffected. diff --git a/packages/app/src/components/handoff/OpenInAgentMenu.non-modal.dom.test.tsx b/packages/app/src/components/handoff/OpenInAgentMenu.non-modal.dom.test.tsx new file mode 100644 index 00000000..3c3a4db3 --- /dev/null +++ b/packages/app/src/components/handoff/OpenInAgentMenu.non-modal.dom.test.tsx @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import type { HandoffDispatchInput } from './useHandoffDispatch'; + +type WindowGlobals = { NodeFilter?: typeof NodeFilter }; +type GlobalWithDomShims = typeof globalThis & + WindowGlobals & { window?: WindowGlobals; ResizeObserver?: unknown }; +const globalWithDomShims = globalThis as GlobalWithDomShims; +if ( + globalWithDomShims.NodeFilter === undefined && + globalWithDomShims.window?.NodeFilter !== undefined +) { + globalWithDomShims.NodeFilter = globalWithDomShims.window.NodeFilter; +} +if (globalWithDomShims.ResizeObserver === undefined) { + class NoopResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + } + globalWithDomShims.ResizeObserver = NoopResizeObserver; +} + +mock.module('@/lib/config-context', () => ({ + useConfigContext: () => ({ merged: null }), +})); +mock.module('./useInstalledAgents', () => ({ + useInstalledAgents: () => ({ states: {}, refresh: () => {} }), +})); +mock.module('./useHandoffDispatch', () => ({ + useHandoffDispatch: () => ({ dispatch: async () => {}, reinstallCoworkSkill: async () => {} }), +})); + +const { OpenInAgentMenu } = await import('./OpenInAgentMenu'); + +const FILE_INPUT: HandoffDispatchInput = { + docContext: null, + projectDir: '/tmp/project', + docPath: 'note.md', +}; + +describe('OpenInAgentMenu non-modal contract', () => { + afterEach(() => { + cleanup(); + document.body.style.pointerEvents = ''; + }); + + test('opening the menu does not disable outside pointer events (body stays interactive)', async () => { + render( {}} />); + + await waitFor(() => { + expect(screen.queryByTestId('open-in-agent-menu')).not.toBeNull(); + }); + + expect(document.body.style.pointerEvents).not.toBe('none'); + }); +}); diff --git a/packages/app/src/components/handoff/OpenInAgentMenu.tsx b/packages/app/src/components/handoff/OpenInAgentMenu.tsx index 3b06500d..4d5c0e38 100644 --- a/packages/app/src/components/handoff/OpenInAgentMenu.tsx +++ b/packages/app/src/components/handoff/OpenInAgentMenu.tsx @@ -76,7 +76,7 @@ export function OpenInAgentMenu({ input, open, onOpenChange }: OpenInAgentMenuPr }; return ( - +