From 6e0bbb5fff6c541c23cc88d6a1c612e9193f30ea Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:35:35 +0200 Subject: [PATCH] fix(site): hide slash skills menu when user has no personal skills (#26953) --- .../ChatMessageInput.stories.tsx | 20 ++++++++++++++-- .../ChatMessageInput/ChatMessageInput.tsx | 23 ++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx index 57d81a4581..70816ee8d1 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.stories.tsx @@ -79,10 +79,26 @@ export const EmptySkills: Story = { }, play: async ({ canvasElement, args }) => { const editor = await typeInEditor(canvasElement, "/"); - expect(await findVisibleText("No personal skills found.")).toBeDefined(); + // "/" is plain text when the skills list is empty. + expectNoVisibleTextImmediately("No personal skills found."); + await userEvent.keyboard("{Enter}"); + expect(args.onEnter).toHaveBeenCalledTimes(1); + expect(editor.textContent).toBe("/"); + }, +}; + +export const FilteredEmptyKeepsMenuOpen: Story = { + args: { + onEnter: fn(), + }, + play: async ({ canvasElement, args }) => { + const editor = await typeInEditor(canvasElement, "/zzzz"); + expect( + await findVisibleText("No personal skills match that query."), + ).toBeDefined(); await userEvent.keyboard("{Enter}"); expect(args.onEnter).not.toHaveBeenCalled(); - expect(editor.textContent).toBe("/"); + expect(editor.textContent).toBe("/zzzz"); }, }; diff --git a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx index a4dfc09bfa..545bdff455 100644 --- a/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx +++ b/site/src/pages/AgentsPage/components/ChatMessageInput/ChatMessageInput.tsx @@ -595,12 +595,25 @@ const ChatMessageInput = ({ useState(null); const suppressedSkillsTriggerRef = useRef(null); const [skillsMenuSelectedIndex, setSkillsMenuSelectedIndex] = useState(0); - const skillsMenuOpen = Boolean(skillsTrigger); + const hasSkillsTrigger = Boolean(skillsTrigger); + const hasPersonalSkillsOverride = personalSkillsOverride !== undefined; const skillsQuery = useQuery({ ...userSkills(), - enabled: skillsMenuOpen && personalSkillsOverride === undefined, + enabled: hasSkillsTrigger && !hasPersonalSkillsOverride, + // Avoid refetching on each trigger toggle from caret movement. + staleTime: 60_000, }); const personalSkills = personalSkillsOverride ?? skillsQuery.data ?? []; + // A stale empty cache with a refetch in flight must not dismiss the menu. + const isResolvedEmptySkillsList = hasPersonalSkillsOverride + ? personalSkills.length === 0 + : skillsQuery.isSuccess && + !skillsQuery.isFetching && + personalSkills.length === 0; + // When the loaded skills list is empty, "/" is plain text. When only + // the filtered result is empty, keep the menu open for the no-match + // message. + const skillsMenuOpen = hasSkillsTrigger && !isResolvedEmptySkillsList; const filteredPersonalSkills = skillsTrigger ? filterPersonalSkills(personalSkills, skillsTrigger.query) : []; @@ -890,7 +903,11 @@ const ChatMessageInput = ({ anchorRect={skillsTrigger?.anchorRect ?? null} query={skillsTrigger?.query ?? ""} skills={filteredPersonalSkills} - isLoading={skillsMenuOpen && skillsQuery.isLoading} + isLoading={ + skillsMenuOpen && + personalSkills.length === 0 && + skillsQuery.isFetching + } onSelectedIndexChange={setSkillsMenuSelectedIndex} isError={skillsMenuOpen && skillsQuery.isError} selectedIndex={selectedSkillIndex}