From a46d8e4bc6b27468c35ed44f5b5be5604a56e643 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 31 May 2026 04:32:52 +0200 Subject: [PATCH] fix(web): align resources picker polish --- apps/web/src/components/ChatComposer.tsx | 439 +++++++++++++++--- apps/web/src/styles/viewer/pets.css | 169 +++---- .../web/src/styles/workspace/mention-home.css | 10 +- .../ChatComposer.context-pickers.test.tsx | 30 +- .../ChatComposer.import-menu.test.tsx | 1 + 5 files changed, 479 insertions(+), 170 deletions(-) diff --git a/apps/web/src/components/ChatComposer.tsx b/apps/web/src/components/ChatComposer.tsx index 7dbbc5ccb..3ae90309e 100644 --- a/apps/web/src/components/ChatComposer.tsx +++ b/apps/web/src/components/ChatComposer.tsx @@ -6,7 +6,9 @@ import { useMemo, useRef, useState, + type KeyboardEvent as ReactKeyboardEvent, type ReactNode, + type Ref, } from "react"; import { createPortal } from 'react-dom'; import { useI18n } from '../i18n'; @@ -434,6 +436,8 @@ export const ChatComposer = forwardRef( const composingRef = useRef(false); const toolsMenuRef = useRef(null); const toolsTriggerRef = useRef(null); + const toolsSearchRef = useRef(null); + const [toolsActiveIndex, setToolsActiveIndex] = useState(0); const petEnabled = Boolean(onAdoptPet && onTogglePet); const [petMenuOpen, setPetMenuOpen] = useState(false); const petWrapRef = useRef(null); @@ -462,6 +466,14 @@ export const ChatComposer = forwardRef( saveComposerDraft(draftStorageKey, draft); }, [draftStorageKey, draft]); + useEffect(() => { + if (!toolsOpen) return; + setToolsActiveIndex(0); + requestAnimationFrame(() => { + toolsSearchRef.current?.focus(); + }); + }, [toolsOpen, toolsTab]); + useEffect(() => { if (!toolsOpen) return; function onPointer(e: MouseEvent) { @@ -2166,12 +2178,6 @@ export const ChatComposer = forwardRef( className="composer-tools-menu" role="menu" > -
- - - {t('chat.resourcesMenuTitle')} - -
{availableTabs.map((tab) => ( @@ -2795,16 +2838,33 @@ function ToolsPluginsPanel({ className={`composer-tools-segment${source === 'mine' ? ' active' : ''}`} onClick={() => setSource('mine')} title={`${userPlugins.length} installed user plugins`} + onKeyDown={(event) => handleResourceKeyboardEvent(event, { + activeIndex: activeResourceIndex, + itemCount: visiblePlugins.length, + onActiveIndexChange, + onPickActive: pickActivePlugin, + })} > My plugins
setQuery(e.currentTarget.value)} + onKeyDown={(event) => handleResourceKeyboardEvent(event, { + activeIndex: activeResourceIndex, + itemCount: visiblePlugins.length, + onActiveIndexChange, + onPickActive: pickActivePlugin, + })} placeholder="Search plugins…" aria-label="Search plugins" + aria-controls="composer-tools-plugin-results" + aria-activedescendant={ + activeResourceIndex >= 0 ? resourceOptionDomId('plugins', activeResourceIndex) : undefined + } /> {visiblePlugins.length === 0 ? ( @@ -2821,17 +2881,22 @@ function ToolsPluginsPanel({ )} ) : ( -
- {visiblePlugins.map((p) => ( -
+ {visiblePlugins.map((p, index) => { + const canShowDetails = pluginHasDetails(p); + return ( +
+
- ))} + ); + })}
)} @@ -2886,11 +2973,17 @@ function ToolsPluginsPanel({ function ToolsMcpPanel({ servers, templates, + activeIndex, + searchRef, + onActiveIndexChange, onInsert, onManage, }: { servers: McpServerConfig[]; templates: McpTemplate[]; + activeIndex: number; + searchRef: Ref; + onActiveIndexChange: (index: number) => void; onInsert: (serverId: string) => void; onManage: () => void; }) { @@ -2903,16 +2996,39 @@ function ToolsMcpPanel({ () => templates.filter((tpl) => mcpTemplateMatchesQuery(tpl, query)).slice(0, 8), [templates, query], ); + const itemCount = visibleServers.length + visibleTemplates.length + 1; + const activeResourceIndex = resourceActiveIndex(activeIndex, itemCount); + const pickActiveResource = () => { + if (activeResourceIndex < 0) return; + if (activeResourceIndex < visibleServers.length) { + onInsert(visibleServers[activeResourceIndex]!.id); + return; + } + onManage(); + }; + const keyboard = (event: ReactKeyboardEvent) => handleResourceKeyboardEvent(event, { + activeIndex: activeResourceIndex, + itemCount, + onActiveIndexChange, + onPickActive: pickActiveResource, + }); + let itemIndex = 0; return ( <>
setQuery(e.currentTarget.value)} + onKeyDown={keyboard} placeholder="Search MCP…" aria-label="Search MCP servers and templates" + aria-controls="composer-tools-mcp-results" + aria-activedescendant={ + activeResourceIndex >= 0 ? resourceOptionDomId('mcp', activeResourceIndex) : undefined + } />
{visibleServers.length === 0 ? ( @@ -2922,61 +3038,84 @@ function ToolsMcpPanel({ : `No configured MCP results for “${query}”.`}
) : ( -
+
Configured
- {visibleServers.map((s) => ( - - ))} + ); + })}
)} {visibleTemplates.length > 0 ? (
Templates
- {visibleTemplates.map((tpl) => ( - - ))} + Manage + + ); + })}
) : null}
) : ( -
- {visibleSkills.map((skill) => { +
+ {visibleSkills.map((skill, index) => { const active = skill.id === currentSkillId; return (