From 3a2a97602ee36a8fd93ef8f7b92214f49886d633 Mon Sep 17 00:00:00 2001 From: Matt Vollmer Date: Fri, 22 May 2026 08:49:00 -0400 Subject: [PATCH] fix(site/src/pages/AgentsPage): dismiss skills trigger on outside click (#25613) When the personal skills menu is open and the user clicks outside (e.g. the send button), the Popover closes via `onOpenChange` but the `SkillsTriggerPlugin`'s `dismissedTriggerRef` is not set. The next Lexical update listener call detects the trigger again and briefly reopens the menu, causing a visible flash. Addresses this symptom: https://github.com/user-attachments/assets/0c1442a2-df75-442b-bcf8-4b028dc647b0 Fix by recording the current trigger position in `dismissedTriggerRef` when the `open` prop transitions from `true` to `false`. This mirrors what the Escape key handler already does and prevents `refreshTrigger` from immediately re-opening the menu at the same position.
Implementation details - Added a `useLayoutEffect` in `SkillsTriggerPlugin` that tracks `open` prop transitions via a `prevOpenRef`. When `open` goes from `true` to `false`, it snapshots the current trigger position into `dismissedTriggerRef`, matching the pattern the Escape handler uses (line 225-227). - Added `OutsideClickDismissesTriggerOnRefocus` Storybook regression story that verifies the menu stays closed when clicking back into the editor after an outside-click dismissal.
--- *PR generated with Coder Agents* --- .../ChatMessageInput/ChatMessageInput.stories.tsx | 15 +++++++++++++++ .../ChatMessageInput/SkillsTriggerPlugin.tsx | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx index fe13c013b1..b28bd912ac 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx @@ -219,3 +219,18 @@ export const OutsideClickClosesWithoutReplacing: Story = { expect(editor.textContent).toBe("/"); }, }; + +export const OutsideClickDismissesTriggerOnRefocus: Story = { + play: async ({ canvasElement }) => { + const editor = await typeInEditor(canvasElement, "/"); + await findVisibleText("/reviewer"); + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: "Outside target" }), + ); + await expectNoVisibleText("/reviewer"); + await userEvent.click(editor); + await expectNoVisibleText("/reviewer"); + expect(editor.textContent).toBe("/"); + }, +}; diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/SkillsTriggerPlugin.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/SkillsTriggerPlugin.tsx index 67194f11e4..cc230d9535 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/SkillsTriggerPlugin.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/SkillsTriggerPlugin.tsx @@ -11,7 +11,7 @@ import { KEY_TAB_COMMAND, type NodeKey, } from "lexical"; -import { useEffect, useEffectEvent, useRef } from "react"; +import { useEffect, useEffectEvent, useLayoutEffect, useRef } from "react"; import type * as TypesGen from "#/api/typesGenerated"; import { parsePersonalSkillTrigger } from "../../utils/personalSkills"; import type { CaretAnchorRect } from "./PersonalSkillsTriggerMenu"; @@ -121,6 +121,16 @@ export const SkillsTriggerPlugin = ({ const [editor] = useLexicalComposerContext(); const dismissedTriggerRef = useRef(null); + const prevOpenRef = useRef(open); + useLayoutEffect(() => { + if (prevOpenRef.current && !open) { + dismissedTriggerRef.current = editor + .getEditorState() + .read(() => activeTriggerFromSelection()); + } + prevOpenRef.current = open; + }, [open, editor]); + const refreshTrigger = useEffectEvent(() => { const trigger = editor.getEditorState().read(() => { return editor.isEditable() ? activeTriggerFromSelection() : null;