From ca91d2d756ac5dd0fa6ff4ef02e0be9596158ba9 Mon Sep 17 00:00:00 2001 From: LiJian Date: Fri, 12 Jun 2026 11:18:44 +0800 Subject: [PATCH] refactor: replace Segmented tabs with SearchBar in ProfileEditor; gate local-system injection (#15593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: activator tool discovery for cloud-sandbox and local-system - P0: Explicitly inject LocalSystemManifest when device gateway is configured (discoverable: isDesktop is always false on server, so it never enters the discovery loop. The explicit injection mirrors the canUseDevice guard.) - P1: Skip CloudSandboxManifest when runtimeMode is not 'cloud' (resolveRuntimeMode unifies executionTarget='sandbox' and legacy chatConfig.runtimeEnv.runtimeMode paths, so agents with sandbox disabled correctly exclude the cloud-sandbox tool.) Both fixes operate at the manifest-map build stage, consistently affecting all downstream consumers (activator discovery, availableTools, etc.) * 🐛 fix: remove cloud-sandbox manifest when runtime is not sandbox The initial manifest seed via getEnabledPluginManifests includes defaultToolIds (which contains lobe-cloud-sandbox), so the manifest was already in toolManifestMap before the allowedBuiltinTools loop's continue guard. This made lobe-cloud-sandbox activatable even when sandbox was disabled. Add a delete right after resolveRuntimeMode to cover both the manifestMap seed and the allowedBuiltinTools loop in one place. Co-authored-by: chatgpt-codex-connector[bot] * ♻️ refactor: replace Segmented tabs with SearchBar in ProfileEditor tool dropdown - PopoverContent: replace Segmented with SearchBar + internal client-side filtering (same pattern as ChatInput ActionBar) - AgentTool: remove ~270 lines of duplicated installedTabItems useMemo; pass unified items - AgentTool: add auto-cleanup for stale plugin identifiers in agent config --- src/features/ProfileEditor/AgentTool.tsx | 407 +++--------------- src/features/ProfileEditor/PopoverContent.tsx | 152 ++++--- 2 files changed, 153 insertions(+), 406 deletions(-) diff --git a/src/features/ProfileEditor/AgentTool.tsx b/src/features/ProfileEditor/AgentTool.tsx index 9ba778fecc..0694fbda9e 100644 --- a/src/features/ProfileEditor/AgentTool.tsx +++ b/src/features/ProfileEditor/AgentTool.tsx @@ -44,8 +44,6 @@ import PopoverContent from './PopoverContent'; const WEB_BROWSING_IDENTIFIER = 'lobe-web-browsing'; -type TabType = 'all' | 'installed'; - export interface AgentToolProps { /** * Optional agent ID to use instead of currentAgentConfig @@ -116,10 +114,6 @@ const AgentTool = memo( const [updating, setUpdating] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); - // Tab state for dual-column layout - const [activeTab, setActiveTab] = useState(null); - const isInitializedRef = useRef(false); - // Fetch plugins const [ useFetchUserKlavisServers, @@ -199,15 +193,6 @@ const AgentTool = memo( [canEdit, toggleWebBrowsing, togglePlugin, showWebBrowsing], ); - // Set default tab based on installed plugins (only on first load) - // Only show 'installed' tab by default if more than 5 plugins are enabled - useEffect(() => { - if (!isInitializedRef.current && plugins.length >= 0) { - isInitializedRef.current = true; - setActiveTab(plugins.length > 5 ? 'installed' : 'all'); - } - }, [plugins.length]); - // Get connected server by identifier const getServerByName = (identifier: string) => { return allKlavisServers.find((server) => server.identifier === identifier); @@ -643,324 +628,6 @@ const AgentTool = memo( [builtinItems, communityGroupChildren, customGroupChildren, t], ); - // Installed tab items - only show enabled items - const installedTabItems: ItemType[] = useMemo(() => { - const items: ItemType[] = []; - - // Enabled builtin tools - const enabledBuiltinItems = filteredBuiltinList - .filter((item) => isToolEnabled(item.identifier)) - .map((item) => ({ - icon: ( - - ), - key: item.identifier, - label: ( - { - setUpdating(true); - await handleToggleTool(item.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - - } - title={t(`tools.builtins.${item.identifier}.title` as any, { - defaultValue: item.meta?.title || item.identifier, - })} - /> - ), - })); - - // Connected and enabled Klavis servers - const connectedKlavisItems = klavisServerItems.filter((item) => - plugins.includes(item.key as string), - ); - - // Connected LobeHub Skill Providers - const connectedLobehubSkillItems = lobehubSkillItems.filter((item) => - plugins.includes(item.key as string), - ); - - // Enabled Builtin Agent Skills - const enabledBuiltinAgentSkillItems = installedBuiltinSkills - .filter((skill) => isToolEnabled(skill.identifier)) - .map((skill) => ({ - icon: skill.avatar ? ( - - ) : ( - - ), - key: skill.identifier, - label: ( - { - setUpdating(true); - await handleToggleTool(skill.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - - ) : ( - - ) - } - /> - ), - })); - - // LobeHub group (Builtin Agent Skills + builtin + LobeHub Skill + Klavis) - const lobehubGroupItems = [ - ...enabledBuiltinAgentSkillItems, - ...enabledBuiltinItems, - ...connectedLobehubSkillItems, - ...connectedKlavisItems, - ]; - - if (lobehubGroupItems.length > 0) { - items.push({ - children: lobehubGroupItems, - key: 'installed-lobehub', - label: t('skillStore.tabs.lobehub'), - type: 'group', - }); - } - - // Enabled community plugins - const enabledCommunityPlugins = communityPlugins - .filter((item) => plugins.includes(item.identifier)) - .map((item) => { - const isMcp = item?.avatar === 'MCP_AVATAR' || !item?.avatar; - return { - icon: isMcp ? ( - - ) : ( - - ), - key: item.identifier, - label: ( - { - setUpdating(true); - await togglePlugin(item.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - - ) : ( - - ) - } - /> - ), - }; - }); - - // Enabled Market Agent Skills - const enabledMarketAgentSkillItems = marketAgentSkills - .filter((skill) => isToolEnabled(skill.identifier)) - .map((skill) => ({ - icon: ( - - ), - key: skill.identifier, - label: ( - { - setUpdating(true); - await handleToggleTool(skill.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - - ), - })); - - // Community group (Market Agent Skills + community plugins) - const allCommunityItems = [...enabledMarketAgentSkillItems, ...enabledCommunityPlugins]; - if (allCommunityItems.length > 0) { - items.push({ - children: allCommunityItems, - key: 'installed-community', - label: t('skillStore.tabs.community'), - type: 'group', - }); - } - - // Enabled custom plugins - const enabledCustomPlugins = customPlugins - .filter((item) => plugins.includes(item.identifier)) - .map((item) => { - const isMcp = item?.avatar === 'MCP_AVATAR' || !item?.avatar; - return { - icon: isMcp ? ( - - ) : ( - - ), - key: item.identifier, - label: ( - { - setUpdating(true); - await togglePlugin(item.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - - ) : ( - - ) - } - /> - ), - }; - }); - - // Enabled User Agent Skills - const enabledUserAgentSkillItems = userAgentSkills - .filter((skill) => isToolEnabled(skill.identifier)) - .map((skill) => ({ - icon: , - key: skill.identifier, - label: ( - { - setUpdating(true); - await handleToggleTool(skill.identifier); - setUpdating(false); - }} - /> - ), - popoverContent: ( - } - identifier={skill.identifier} - sourceLabel={t('skillStore.tabs.custom')} - title={skill.name} - /> - ), - })); - - // Custom group (User Agent Skills + custom plugins) - const allCustomItems = [...enabledUserAgentSkillItems, ...enabledCustomPlugins]; - if (allCustomItems.length > 0) { - items.push({ - children: allCustomItems, - key: 'installed-custom', - label: t('skillStore.tabs.custom'), - type: 'group', - }); - } - - return items; - }, [ - filteredBuiltinList, - installedBuiltinSkills, - marketAgentSkills, - userAgentSkills, - klavisServerItems, - lobehubSkillItems, - communityPlugins, - customPlugins, - plugins, - isToolEnabled, - handleToggleTool, - togglePlugin, - t, - ]); - - // Use effective tab for display (default to all while initializing) - const effectiveTab = activeTab ?? 'all'; - const button = ( ); + // ────────────────────────────────────────────── + // Auto-cleanup stale plugins that no longer exist + // ────────────────────────────────────────────── + // Build the set of all valid identifiers known to the system + const validIdentifiers = useMemo(() => { + const all = new Set(); + + // 1. Builtin tools (includes Klavis metas) + for (const tool of builtinList) all.add(tool.identifier); + + // 2. Installed plugins + for (const plugin of installedPluginList) all.add(plugin.identifier); + + // 3. Klavis server types (if enabled) + if (isKlavisEnabledInEnv) { + for (const type of KLAVIS_SERVER_TYPES) all.add(type.identifier); + } + + // 4. LobeHub Skill providers (if enabled) + if (isLobehubSkillEnabled) { + for (const provider of LOBEHUB_SKILL_PROVIDERS) all.add(provider.id); + } + + // 5. Builtin skills + for (const skill of installedBuiltinSkills) all.add(skill.identifier); + + // 6. Market agent skills + for (const skill of marketAgentSkills) all.add(skill.identifier); + + // 7. User agent skills + for (const skill of userAgentSkills) all.add(skill.identifier); + + return all; + }, [ + builtinList, + installedPluginList, + isKlavisEnabledInEnv, + isLobehubSkillEnabled, + installedBuiltinSkills, + marketAgentSkills, + userAgentSkills, + ]); + + // Track whether initial cleanup has been performed + const cleanupDoneRef = useRef(false); + + // Auto-remove stale plugin IDs from the agent config + // Uses a short debounce to allow async data (SWR) to complete loading + useEffect(() => { + if (cleanupDoneRef.current) return; + if (validIdentifiers.size === 0) return; + if (plugins.length === 0) return; + + // Defer cleanup to avoid race with async data loading (SWR, Klavis, etc.) + const timer = setTimeout(() => { + const stalePlugins = plugins.filter((id) => !validIdentifiers.has(id)); + + if (stalePlugins.length > 0 && effectiveAgentId) { + const cleanedPlugins = plugins.filter((id) => validIdentifiers.has(id)); + updateAgentConfigById(effectiveAgentId, { plugins: cleanedPlugins }); + } + + cleanupDoneRef.current = true; + }, 500); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [validIdentifiers]); + // Combine plugins and web browsing for display const allEnabledTools = useMemo(() => { const tools = [...plugins]; @@ -1011,11 +747,8 @@ const AgentTool = memo( }} popupRender={() => ( setDropdownOpen(false)} - onTabChange={setActiveTab} onOpenStore={() => { setDropdownOpen(false); createSkillStoreModal(); diff --git a/src/features/ProfileEditor/PopoverContent.tsx b/src/features/ProfileEditor/PopoverContent.tsx index d9525be7a9..faa63d99d6 100644 --- a/src/features/ProfileEditor/PopoverContent.tsx +++ b/src/features/ProfileEditor/PopoverContent.tsx @@ -1,8 +1,8 @@ import { type ItemType } from '@lobehub/ui'; -import { Flexbox, Icon, Segmented, stopPropagation } from '@lobehub/ui'; +import { Flexbox, Icon, SearchBar, stopPropagation } from '@lobehub/ui'; import { createStaticStyles, cssVar } from 'antd-style'; import { ChevronRight, ExternalLink, Settings, Store } from 'lucide-react'; -import { memo } from 'react'; +import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ScrollSignalProvider } from '@/features/ChatInput/ActionBar/Tools/ScrollSignalContext'; @@ -11,18 +11,42 @@ import { useWorkspaceAwareNavigate } from '@/features/Workspace/useWorkspaceAwar import Empty from './Empty'; -type TabType = 'all' | 'installed'; - const SKILL_ICON_SIZE = 20; +const filterItems = (items: ItemType[], keyword: string): ItemType[] => { + const lower = keyword.toLowerCase(); + + return items + .map((item) => { + if (!item) return null; + + if (item.type === 'group' && 'children' in item && item.children) { + const filtered = item.children.filter((child) => { + if (!child) return false; + const key = String(child.key || '').toLowerCase(); + return key.includes(lower); + }); + if (filtered.length === 0) return null; + return { ...item, children: filtered }; + } + + if (item.type === 'divider') return item; + + const key = String('key' in item ? item.key : '').toLowerCase(); + return key.includes(lower) ? item : null; + }) + .filter(Boolean) as ItemType[]; +}; + const styles = createStaticStyles(({ css }) => ({ footer: css` padding: 4px; border-block-start: 1px solid ${cssVar.colorBorderSecondary}; `, header: css` - padding: ${cssVar.paddingXS}; - border-block-end: 1px solid ${cssVar.colorBorderSecondary}; + padding-block: 8px; + padding-inline: 8px; + border-block-end: 1px solid ${cssVar.colorFill}; background: transparent; `, scroller: css` @@ -34,77 +58,67 @@ const styles = createStaticStyles(({ css }) => ({ })); interface PopoverContentProps { - activeTab: TabType; - allTabItems: ItemType[]; - installedTabItems: ItemType[]; + items: ItemType[]; onClose?: () => void; onOpenStore: () => void; - onTabChange: (tab: TabType) => void; } -const PopoverContent = memo( - ({ activeTab, onTabChange, allTabItems, installedTabItems, onOpenStore, onClose }) => { - const { t } = useTranslation('setting'); - const navigate = useWorkspaceAwareNavigate(); +const PopoverContent = memo(({ items, onOpenStore, onClose }) => { + const { t } = useTranslation('setting'); + const navigate = useWorkspaceAwareNavigate(); + const [searchKeyword, setSearchKeyword] = useState(''); - const currentItems = activeTab === 'all' ? allTabItems : installedTabItems; + const filteredItems = useMemo( + () => (searchKeyword ? filterItems(items, searchKeyword) : items), + [items, searchKeyword], + ); - return ( - - {/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */} -
- onTabChange(v as TabType)} - /> -
- - {activeTab === 'installed' && installedTabItems.length === 0 ? ( - - ) : ( - - )} - -
-
-
- -
-
{t('skillStore.title')}
- -
-
{ - onClose?.(); - navigate('/settings/skill'); - }} - > -
- -
-
{t('tools.plugins.management')}
- + const isEmpty = filteredItems.length === 0; + + return ( + +
+ setSearchKeyword(e.target.value)} + onKeyDown={stopPropagation} + /> +
+ + {isEmpty ? : } + +
+
+
+
+
{t('skillStore.title')}
+
- - ); - }, -); +
{ + onClose?.(); + navigate('/settings/skill'); + }} + > +
+ +
+
{t('tools.plugins.management')}
+ +
+
+
+ ); +}); PopoverContent.displayName = 'PopoverContent';