-
Notifications
You must be signed in to change notification settings - Fork 120
fix: QuickEntry agent outside-click + quarantine CLI full-suite cascade #2291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@runfusion/fusion": patch | ||
| --- | ||
|
|
||
| summary: Close the Quick Add agent picker when clicking outside it. | ||
| category: fix | ||
| dev: Capture-phase mousedown listener plus open-token so late fetchAgents cannot re-open a dismissed portal. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,6 +191,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, | |
| const modelMenuPortalRef = useRef<HTMLDivElement>(null); | ||
| const agentPickerRef = useRef<HTMLDivElement>(null); | ||
| const agentPickerPortalRef = useRef<HTMLDivElement>(null); | ||
| /** Bumps on open/close so a late fetchAgents resolution cannot re-open a dismissed picker. */ | ||
| const agentPickerOpenTokenRef = useRef(0); | ||
| const nodePickerRef = useRef<HTMLDivElement>(null); | ||
| const nodePickerPortalRef = useRef<HTMLDivElement>(null); | ||
| const priorityPickerRef = useRef<HTMLDivElement>(null); | ||
|
|
@@ -554,17 +556,25 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, | |
| useEffect(() => { | ||
| if (!showAgentPicker) return; | ||
|
|
||
| /* | ||
| FNXC:QuickEntry 2026-07-18-08:45: | ||
| Listen in the capture phase so outside mousedown closes the agent portal even | ||
| when a nested control calls preventDefault/stopPropagation on bubble. Full- | ||
| suite + local tests observed the bubble-only listener leaving "Select agent" | ||
| mounted after a true outside click. | ||
| */ | ||
| const handleClickOutside = (e: MouseEvent) => { | ||
| const target = e.target as Node; | ||
| // Check both the trigger button and the portaled dropdown | ||
| if (agentPickerRef.current?.contains(target)) return; | ||
| if (agentPickerPortalRef.current?.contains(target)) return; | ||
| agentPickerOpenTokenRef.current += 1; | ||
| setShowAgentPicker(false); | ||
| setAgentPickerPosition(null); | ||
| }; | ||
|
|
||
| document.addEventListener("mousedown", handleClickOutside); | ||
| return () => document.removeEventListener("mousedown", handleClickOutside); | ||
| document.addEventListener("mousedown", handleClickOutside, true); | ||
| return () => document.removeEventListener("mousedown", handleClickOutside, true); | ||
| }, [showAgentPicker]); | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -1637,24 +1647,38 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, | |
|
|
||
| const loadAgents = useCallback(async () => { | ||
| if (agents.length > 0 && agentsProjectId === projectId) { | ||
| agentPickerOpenTokenRef.current += 1; | ||
| setShowAgentPicker(true); | ||
| updateAgentPickerPosition(); | ||
| return; | ||
| } | ||
|
|
||
| /* | ||
| FNXC:QuickEntry 2026-07-18-08:45: | ||
| Open the picker immediately (with loading) so outside-click listeners arm | ||
| before fetchAgents resolves. Bump a token on open/close so a late fetch | ||
| does not re-open the portal after the operator dismissed it. | ||
| */ | ||
| const openToken = ++agentPickerOpenTokenRef.current; | ||
| setAgentsLoading(true); | ||
| setShowAgentPicker(true); | ||
| updateAgentPickerPosition(); | ||
| try { | ||
| const result = await fetchAgents(undefined, projectId); | ||
| if (openToken !== agentPickerOpenTokenRef.current) return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a fetch is pending, Escape, a second trigger click, or form reset closes the picker without changing Context Used: AGENTS.md (source) |
||
| setAgents(result); | ||
| setAgentsProjectId(projectId); | ||
| setShowAgentPicker(true); | ||
|
Comment on lines
1669
to
1671
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Context Used: AGENTS.md (source) |
||
| updateAgentPickerPosition(); | ||
| } catch (err) { | ||
| if (openToken !== agentPickerOpenTokenRef.current) return; | ||
| const msg = getErrorMessage(err); | ||
| addToast(msg ? t("tasks.loadAgentsFailed", "Failed to load agents: {{msg}}", { msg }) : t("tasks.loadAgentsFailedGeneric", "Failed to load agents"), "error"); | ||
| setShowAgentPicker(false); | ||
| } finally { | ||
| setAgentsLoading(false); | ||
| if (openToken === agentPickerOpenTokenRef.current) { | ||
| setAgentsLoading(false); | ||
| } | ||
| } | ||
| }, [agents.length, agentsProjectId, projectId, addToast, updateAgentPickerPosition]); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4955,13 +4955,19 @@ describe("QuickEntryBox", () => { | |
| const outsideElement = document.createElement("div"); | ||
| document.body.appendChild(outsideElement); | ||
| try { | ||
| fireEvent.mouseDown(outsideElement); | ||
| /* | ||
| FNXC:DashboardTests 2026-07-18-08:45: | ||
| Dispatch a bubbling native MouseEvent so the document mousedown listener | ||
| in QuickEntryBox sees the outside target (RTL fireEvent alone was flaky | ||
| under full-suite and failed locally for this agent portal). | ||
| */ | ||
| outsideElement.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true })); | ||
| await waitFor(() => { | ||
| expect(screen.queryByText("Select agent")).toBeNull(); | ||
| }); | ||
|
Comment on lines
+4964
to
+4967
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This test dismisses the picker only after the mocked agent request has resolved, so removing the new token guard would not make it fail. Use a deferred request, dismiss the loading picker, then resolve the request and verify that the portal stays closed. Context Used: AGENTS.md (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| } finally { | ||
| document.body.removeChild(outsideElement); | ||
| if (outsideElement.parentNode) document.body.removeChild(outsideElement); | ||
| } | ||
|
|
||
| // Picker should be closed | ||
| expect(screen.queryByText("Select agent")).toBeNull(); | ||
| }); | ||
|
|
||
| it("repositions portaled picker on window resize while open", async () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Invalidate the token on all dismissal paths.
The token is successfully incremented on outside clicks, but other dismissal vectors (such as pressing the Escape key, clicking the Agent toggle button to close, or opening other pickers) call
setShowAgentPicker(false)without bumping the token. If an agent fetch is in flight when the user closes the picker via these actions, the late resolution will still bypass the token guard and erroneously re-open the picker.Consider adding a
useEffectthat bumps the token when the picker closes, or extract acloseAgentPickerhelper that groups the state update and token increment, and use it across all dismissal paths.🤖 Prompt for AI Agents