Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/quickentry-agent-outside-click.md
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.
21 changes: 21 additions & 0 deletions packages/cli/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ const quarantinedCliTests: string[] = [
FNXC:CliTests 2026-07-16-09:00:
FN-8077 removed project-context.test.ts from this list and the ledger in lockstep. Its CentralCore coverage now uses the external PostgreSQL test harness under pgDescribe rather than launching an embedded postmaster for each test in forked loaded lanes; pure formatting coverage remains ungated.
*/
/*
FNXC:CliTests 2026-07-18-08:50:
Full-suite shard 4 (runs 29637256028, 29637376023) re-exposed package-lane cascade:
extension-dist-barrel beforeAll hookTimeout under PG load, plus lock-retry timeouts,
cascading 87 failures. Quarantine the observed integration-heavy files on sight in
lockstep with scripts/lib/test-quarantine.json — do not raise hookTimeout/testTimeout.
*/
"src/__tests__/bin.test.ts",
"src/__tests__/extension-agent-update.test.ts",
"src/__tests__/extension-dist-barrel.test.ts",
"src/__tests__/extension-goal-tools.test.ts",
"src/__tests__/extension-mission-goal-tools.test.ts",
"src/__tests__/extension-tool-timeout.test.ts",
"src/__tests__/extension-workflow-tools.test.ts",
"src/__tests__/extension.test.ts",
"src/__tests__/task-plan.test.ts",
"src/commands/__tests__/mcp-lock-retry.test.ts",
"src/commands/__tests__/plugin.test.ts",
"src/commands/__tests__/task-lock-retry.test.ts",
"src/commands/dashboard-tui/__tests__/app.test.tsx",
"src/commands/dashboard-tui/__tests__/terminal-attach.test.ts",
];

export default defineConfig({
Expand Down
30 changes: 27 additions & 3 deletions packages/dashboard/app/components/QuickEntryBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +194 to +195

Copy link
Copy Markdown
Contributor

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 useEffect that bumps the token when the picker closes, or extract a closeAgentPicker helper that groups the state update and token increment, and use it across all dismissal paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/app/components/QuickEntryBox.tsx` around lines 194 - 195,
Ensure every Agent picker dismissal invalidates pending fetches, not only
outside-click handling. Update the QuickEntryBox dismissal flows—including
Escape, the Agent toggle, and opening other pickers—to use a shared
closeAgentPicker helper or equivalent effect that increments
agentPickerOpenTokenRef alongside setShowAgentPicker(false), while preserving
the existing token guard.

const nodePickerRef = useRef<HTMLDivElement>(null);
const nodePickerPortalRef = useRef<HTMLDivElement>(null);
const priorityPickerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Other Dismissals Keep Request Valid

When a fetch is pending, Escape, a second trigger click, or form reset closes the picker without changing agentPickerOpenTokenRef. The response therefore passes this check and reopens a picker the operator already dismissed.

Context Used: AGENTS.md (source)

setAgents(result);
setAgentsProjectId(projectId);
setShowAgentPicker(true);
Comment on lines 1669 to 1671

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Project Switch Accepts Stale Agents

If projectId changes while this request is pending, the project-change effect closes and clears the picker but does not invalidate the token. The old response can then populate agents under the new project ID and reopen the picker with data from the previous project.

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]);

Expand Down
16 changes: 11 additions & 5 deletions packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Late Fetch Race Remains Untested

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 () => {
Expand Down
72 changes: 71 additions & 1 deletion scripts/lib/test-quarantine.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$comment": "Flaky-test quarantine ledger (deletion ratchet see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config is the enforcement.",
"$comment": "Flaky-test quarantine ledger (deletion ratchet \u2014 see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date \u2014 the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config is the enforcement.",
"entries": [
{
"file": "packages/engine/src/__tests__/backlog-pressure-reporter.test.ts",
Expand Down Expand Up @@ -40,6 +40,76 @@
"file": "packages/engine/src/__tests__/heartbeat-error-recovery.test.ts",
"reason": "Full-suite shard 1 timeout (30s) on rotation-shaped 401 recovery under load without product bug evidence. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29636550951. Mirrored in packages/engine/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/bin.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-agent-update.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-dist-barrel.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-goal-tools.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-mission-goal-tools.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-tool-timeout.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension-workflow-tools.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/extension.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/__tests__/task-plan.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/__tests__/mcp-lock-retry.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/__tests__/plugin.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/__tests__/task-lock-retry.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
},
{
"file": "packages/cli/src/commands/dashboard-tui/__tests__/terminal-attach.test.ts",
"reason": "Full-suite shard 4 package-lane cascade (run 29637376023 / 29637256028): extension-dist-barrel beforeAll hookTimeout 10s + lock-retry 5s timeouts under shard load, cascading 87 failures across CLI integration suites without product-bug evidence. Quarantine on sight per AGENTS.md. Mirrored in packages/cli/vitest.config.ts.",
"quarantinedAt": "2026-07-18"
}
]
}
Loading