From c3cebcd56b897dab65593297aee374e7e70448a0 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 24 Jul 2026 00:35:34 -0700 Subject: [PATCH] FN-8556: fix Windows agent target selection Make direct-chat Agent selection reliable when Windows Electron suppresses the click after pointer activation. - Switch target mode on primary pointerdown while retaining keyboard activation. - Expose selected Model and Agent modes with aria-pressed semantics. - Cover pointer activation and persistence across direct-chat layouts. - Add a patch changeset for the Windows selection fix. Files changed: .changeset/fn-8556-windows-agent-selection.md | 7 ++++ .../app/components/ChatThinkingLevelControl.tsx | 28 ++++++++++++- .../__tests__/ChatThinkingLevelControl.test.tsx | 27 +++++++++++-- .../__tests__/ChatView.thinking-level.test.tsx | 46 +++++++++++++++++----- 4 files changed, 93 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-8556 Fusion-Task-Lineage: 25b06177-3b59-4d2d-8440-e92fe7172259 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8556-windows-agent-selection.md | 7 +++ .../components/ChatThinkingLevelControl.tsx | 28 ++++++++++- .../ChatThinkingLevelControl.test.tsx | 25 +++++++++- .../ChatView.thinking-level.test.tsx | 46 +++++++++++++++---- 4 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 .changeset/fn-8556-windows-agent-selection.md diff --git a/.changeset/fn-8556-windows-agent-selection.md b/.changeset/fn-8556-windows-agent-selection.md new file mode 100644 index 0000000000..17254c74a9 --- /dev/null +++ b/.changeset/fn-8556-windows-agent-selection.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make Windows direct-chat Agent selection switch visibly and reliably. +category: fix +dev: Handles primary pointer activation before an Electron host can suppress its following click. diff --git a/packages/dashboard/app/components/ChatThinkingLevelControl.tsx b/packages/dashboard/app/components/ChatThinkingLevelControl.tsx index 0a5e37dd7b..4fc945271f 100644 --- a/packages/dashboard/app/components/ChatThinkingLevelControl.tsx +++ b/packages/dashboard/app/components/ChatThinkingLevelControl.tsx @@ -139,6 +139,18 @@ export function ChatThinkingLevelControl({ void onChangeModel?.({ agentId: nextAgentId }); }; + /* + FNXC:Chat-ModelSwitch 2026-07-24-00:00: + Windows Electron can show the Agent toggle's pressed feedback after primary pointerdown while + its host prevents the following click. Commit the visual mode switch on primary pointerdown so + the available-agent list deterministically replaces the model picker; preserve click handling + only for keyboard/synthetic activation (`detail === 0`) so one pointer gesture does not reset + the local mode twice. `aria-pressed` makes the selected target observable to assistive tech. + */ + const activateTargetMode = (mode: TargetMode) => { + setTargetMode(mode); + }; + const handleTriggerKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false); @@ -197,7 +209,13 @@ export function ChatThinkingLevelControl({ type="button" className={`chat-thinking-mode-btn${targetMode === "model" ? " chat-thinking-mode-btn--active" : ""}`} data-testid="chat-thinking-mode-model" - onClick={() => setTargetMode("model")} + aria-pressed={targetMode === "model"} + onPointerDown={(event) => { + if (event.button === 0) activateTargetMode("model"); + }} + onClick={(event) => { + if (event.detail === 0) activateTargetMode("model"); + }} > {t("chat.newChatModeModel", "Model")} @@ -205,7 +223,13 @@ export function ChatThinkingLevelControl({ type="button" className={`chat-thinking-mode-btn${targetMode === "agent" ? " chat-thinking-mode-btn--active" : ""}`} data-testid="chat-thinking-mode-agent" - onClick={() => setTargetMode("agent")} + aria-pressed={targetMode === "agent"} + onPointerDown={(event) => { + if (event.button === 0) activateTargetMode("agent"); + }} + onClick={(event) => { + if (event.detail === 0) activateTargetMode("agent"); + }} > {t("chat.newChatModeAgent", "Agent")} diff --git a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx index 3cb640c3d7..6480d3f4b5 100644 --- a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx @@ -126,15 +126,36 @@ describe("ChatThinkingLevelControl", () => { expect(screen.queryByRole("listbox")).toBeNull(); }); - it("the Model|Agent toggle swaps controls", () => { + it("switches to Agent mode during the Windows pointer activation sequence and exposes selected semantics", () => { render(); fireEvent.click(screen.getByTestId("chat-thinking-btn")); + const modelMode = screen.getByTestId("chat-thinking-mode-model"); + const agentMode = screen.getByTestId("chat-thinking-mode-agent"); expect(screen.getByTestId("mock-model-dropdown")).toBeDefined(); + expect(modelMode).toHaveAttribute("aria-pressed", "true"); + expect(agentMode).toHaveAttribute("aria-pressed", "false"); - fireEvent.click(screen.getByTestId("chat-thinking-mode-agent")); + // Electron on Windows visibly dispatches pointerdown before it completes click activation. + // Agent mode must render from that primary-pointer boundary, not wait for a click that a + // host surface can suppress after showing the pressed state. + fireEvent.pointerDown(agentMode, { button: 0, pointerType: "mouse" }); + expect(agentMode).toHaveClass("chat-thinking-mode-btn--active"); + expect(agentMode).toHaveAttribute("aria-pressed", "true"); + expect(modelMode).toHaveAttribute("aria-pressed", "false"); + expect(screen.queryByTestId("chat-thinking-model-picker")).toBeNull(); expect(screen.getByTestId("chat-thinking-agent-list")).toBeDefined(); expect(screen.getByTestId("chat-thinking-agent-agent-001")).toBeDefined(); + + fireEvent.pointerUp(agentMode, { button: 0, pointerType: "mouse" }); + fireEvent.click(agentMode, { detail: 1 }); + expect(screen.getByTestId("chat-thinking-agent-list")).toBeDefined(); + + // Keyboard button activation remains a click with no pointer detail. + fireEvent.click(modelMode, { detail: 0 }); + expect(modelMode).toHaveClass("chat-thinking-mode-btn--active"); + expect(modelMode).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByTestId("chat-thinking-model-picker")).toBeDefined(); }); it("selecting a model calls onChangeModel with the provider/model pair and closes", () => { diff --git a/packages/dashboard/app/components/__tests__/ChatView.thinking-level.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.thinking-level.test.tsx index e762739487..e8498f3b3c 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.thinking-level.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.thinking-level.test.tsx @@ -242,7 +242,12 @@ describe("ChatView thinking-level control (FN-7898)", () => { await renderWithAct(); fireEvent.click(screen.getByTestId("chat-thinking-btn")); - fireEvent.click(screen.getByTestId("chat-thinking-mode-agent")); + const agentMode = screen.getByTestId("chat-thinking-mode-agent"); + fireEvent.pointerDown(agentMode, { button: 0, pointerType: "mouse" }); + fireEvent.pointerUp(agentMode, { button: 0, pointerType: "mouse" }); + fireEvent.click(agentMode, { detail: 1 }); + expect(agentMode).toHaveAttribute("aria-pressed", "true"); + expect(screen.queryByTestId("chat-thinking-model-picker")).toBeNull(); fireEvent.click(screen.getByTestId("chat-thinking-agent-agent-002")); expect(setSessionModel).toHaveBeenCalledWith("sess-agent", { agentId: "agent-002" }); @@ -313,19 +318,40 @@ describe("ChatView thinking-level control (FN-7898)", () => { expect(screen.getByTestId("chat-thinking-agent-agent-001").className).toContain("chat-thinking-agent-item--selected"); }); - it("(f) renders the trigger without a layout/overflow regression at a mobile viewport", async () => { - mockMobileViewport(); - const session = makeSession({ id: "sess-mobile", cliExecutorAdapterId: null, thinkingLevel: null }); - mockUseChat.mockReturnValue(chatState({ activeSession: session, sessions: [session] })); + it.each([ + { name: "mobile", configure: mockMobileViewport, props: {} }, + { name: "narrow floating", configure: mockDesktopViewport, props: { floating: true, compactLayout: true } }, + ])("(f) switches to and persists an Agent target from the $name direct-chat host", async ({ name, configure, props }) => { + configure(); + const setSessionModel = vi.fn(); + const session = makeSession({ id: `sess-${name.replaceAll(" ", "-")}`, cliExecutorAdapterId: null, agentId: useChatModule.FN_AGENT_ID }); + const agentsMap = new Map([ + ["agent-001", { id: "agent-001", name: "Alpha", role: "executor" }], + ["agent-002", { id: "agent-002", name: "Beta", role: "reviewer" }], + ] as const); + mockUseChat.mockReturnValue(chatState({ activeSession: session, sessions: [session], agentsMap, setSessionModel })); - await renderWithAct(); + await renderWithAct(); + + expect(document.querySelector(".chat-view--narrow")).toBeTruthy(); + if (props.floating) expect(document.querySelector(".chat-view--floating")).toBeTruthy(); const trigger = screen.getByTestId("chat-thinking-btn"); - expect(trigger).toBeInTheDocument(); - // Shell/structure assertion consistent with other ChatView mobile tests: - // the trigger sits alongside the attach button inside the same input row. expect(trigger.closest(".chat-input-row")).not.toBeNull(); - expect(trigger.closest(".chat-thinking-level-root")).not.toBeNull(); + fireEvent.click(trigger); + + // Match the Windows Electron pointer activation whose click can be consumed by a host. + const agentMode = screen.getByTestId("chat-thinking-mode-agent"); + fireEvent.pointerDown(agentMode, { button: 0, pointerType: "mouse" }); + fireEvent.pointerUp(agentMode, { button: 0, pointerType: "mouse" }); + fireEvent.click(agentMode, { detail: 1 }); + + expect(agentMode).toHaveAttribute("aria-pressed", "true"); + expect(screen.queryByTestId("chat-thinking-model-picker")).toBeNull(); + expect(screen.getByTestId("chat-thinking-agent-list")).toBeInTheDocument(); + fireEvent.click(screen.getByTestId("chat-thinking-agent-agent-002")); + + expect(setSessionModel).toHaveBeenCalledWith(session.id, { agentId: "agent-002" }); }); it("(g) uses the resolved Settings default for both the in-chat and New Chat thinking-level labels", async () => {