diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx index e06a9da113..5acf42671c 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx @@ -76,6 +76,13 @@ const expectNoVisibleText = async (text: string) => { }); }; +const expectNoVisibleTextImmediately = (text: string) => { + const matches = within(document.body).queryAllByText(text); + expect( + matches.every((element) => element.getClientRects().length === 0), + ).toBe(true); +}; + const editorFromCanvas = (canvasElement: HTMLElement) => { const canvas = within(canvasElement); return canvas.getByTestId("chat-message-input"); @@ -192,6 +199,18 @@ export const SlashInsideUrlDoesNotOpen: Story = { }, }; +export const BackspaceClosesWithoutEmptyStateFlash: Story = { + play: async ({ canvasElement }) => { + const editor = await typeInEditor(canvasElement, "/"); + await findVisibleText("/reviewer"); + await userEvent.keyboard("{Backspace}"); + + expect(editor.textContent).toBe(""); + expectNoVisibleTextImmediately("No personal skills found."); + await expectNoVisibleText("/reviewer"); + }, +}; + export const EscapeClosesWithoutReplacing: Story = { play: async ({ canvasElement }) => { const editor = await typeInEditor(canvasElement, "/"); diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/PersonalSkillsTriggerMenu.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/PersonalSkillsTriggerMenu.tsx index 0c8dba1394..ae0b509bbd 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/PersonalSkillsTriggerMenu.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/PersonalSkillsTriggerMenu.tsx @@ -1,3 +1,4 @@ +import { useLayoutEffect, useState } from "react"; import type * as TypesGen from "#/api/typesGenerated"; import { Command, @@ -35,6 +36,15 @@ type PersonalSkillsTriggerMenuProps = { onClose: () => void; }; +type PersonalSkillsMenuState = { + anchorRect: CaretAnchorRect; + query: string; + skills: readonly TypesGen.UserSkillMetadata[]; + isLoading?: boolean; + isError?: boolean; + selectedIndex: number; +}; + export const PersonalSkillsTriggerMenu = ({ open, anchorRect, @@ -47,34 +57,72 @@ export const PersonalSkillsTriggerMenu = ({ onSelect, onClose, }: PersonalSkillsTriggerMenuProps) => { + const [lastOpenMenuState, setLastOpenMenuState] = + useState(null); + const isAnchoredOpen = open && anchorRect !== null; + const activeMenuState: PersonalSkillsMenuState | null = isAnchoredOpen + ? { + anchorRect, + query, + skills, + isLoading, + isError, + selectedIndex, + } + : null; + const menuState = activeMenuState ?? lastOpenMenuState; + const menuAnchorRect = menuState?.anchorRect ?? null; + const menuSkills = menuState?.skills ?? []; + const menuSelectedIndex = menuState?.selectedIndex ?? -1; + + useLayoutEffect(() => { + if (!isAnchoredOpen) { + return; + } + setLastOpenMenuState({ + anchorRect, + query, + skills, + isLoading, + isError, + selectedIndex, + }); + }, [ + anchorRect, + isAnchoredOpen, + isError, + isLoading, + query, + selectedIndex, + skills, + ]); + const handleHighlightedValueChange = (value: string) => { - const nextIndex = skills.findIndex((skill) => skill.name === value); + const nextIndex = menuSkills.findIndex((skill) => skill.name === value); if (nextIndex >= 0) { onSelectedIndexChange(nextIndex); } }; - const shouldRender = open && anchorRect; - return ( { if (!nextOpen) { onClose(); } }} > - {shouldRender && ( + {menuAnchorRect && (