diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 7ea5b9ab..39a4754f 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -29,6 +29,8 @@ import { FilePlus2, Sparkles, HardDrive, + Server, + Puzzle, } from 'lucide-react'; import { useTheme } from '@/components/providers/theme-provider'; @@ -370,7 +372,23 @@ function NavItems({ // Entity categories: collapsible with sub-items const entityKey = ENTITY_KEY_MAP[config.id]; - const items: SidebarEntityItem[] = sidebarData[entityKey]; + const isExtensionsCategory = config.id === 'plugins'; + const items: SidebarEntityItem[] = isExtensionsCategory + ? [ + ...sidebarData.plugins.map((p) => ({ + ...p, + extensionType: 'plugin' as const, + })), + ...sidebarData.mcpServers.map((m) => ({ + ...m, + extensionType: 'mcp' as const, + })), + ...sidebarData.skills.map((s) => ({ + ...s, + extensionType: 'skill' as const, + })), + ] + : sidebarData[entityKey]; const routePrefix = ENTITY_ROUTE_MAP[config.id]; const hasDetailPages = DETAIL_PAGE_CATEGORIES.includes(config.id); const canCreate = CREATABLE_CATEGORIES.includes(config.id); @@ -379,6 +397,18 @@ function NavItems({ const isSkill = config.id === 'skills'; const isBot = config.id === 'bots'; const isMCP = config.id === 'mcp'; + + const resolveItemRoute = (item: SidebarEntityItem): string => { + if (item.extensionType === 'mcp') { + return `/home/mcp?id=${encodeURIComponent(item.id)}`; + } + if (item.extensionType === 'skill') { + return `/home/skills?id=${encodeURIComponent(item.id)}`; + } + return hasDetailPages + ? `${routePrefix}?id=${encodeURIComponent(item.id)}` + : routePrefix; + }; const isActive = selectedChild?.id === config.id || pathname === routePrefix || @@ -394,7 +424,13 @@ function NavItems({ // Shared entity list renderer used by both popover and collapsible const renderEntityList = (inPopover: boolean) => { - const sortedItems = sortByRecent(items); + const sortedItems = isExtensionsCategory + ? [...items].sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }), + ) + : sortByRecent(items); const isExpanded = expandedLists[config.id] ?? false; const maxItems = inPopover ? 10 : MAX_VISIBLE_ITEMS; const visibleItems = @@ -416,152 +452,212 @@ function NavItems({ ); } - return ( - <> - {visibleItems.map((item) => { - const itemRoute = hasDetailPages - ? `${routePrefix}?id=${encodeURIComponent(item.id)}` - : routePrefix; - const isItemActive = - hasDetailPages && - pathname === routePrefix && - searchParams.get('id') === item.id; + const itemActiveCheck = (item: SidebarEntityItem): boolean => { + if (item.extensionType === 'mcp') { + return ( + pathname === '/home/mcp' && searchParams.get('id') === item.id + ); + } + if (item.extensionType === 'skill') { + return ( + pathname === '/home/skills' && + searchParams.get('id') === item.id + ); + } + return ( + hasDetailPages && + pathname === routePrefix && + searchParams.get('id') === item.id + ); + }; - if (inPopover) { - return ( - - ); - } + )} + + ) : item.extensionType === 'plugin' ? ( + + ) : isMCP ? ( + + ) : null} + {item.name} + + ); + } - // Normal sidebar sub-item rendering - return ( - - - - - { - e.preventDefault(); - navigate(itemRoute); - }} - > - {item.emoji ? ( - - {item.emoji} - - ) : item.iconURL ? ( - - - {(isBot || isMCP) && ( - - )} - - ) : isMCP ? ( + // Normal sidebar sub-item rendering + return ( + + + + + { + e.preventDefault(); + navigate(itemRoute); + }} + > + {item.extensionType === 'mcp' ? ( + + ) : item.extensionType === 'skill' ? ( + + ) : item.emoji ? ( + {item.emoji} + ) : item.iconURL ? ( + + + {(isBot || isMCP) && ( - ) : null} - {item.name} - {item.debug && ( - )} - - - - {item.description && ( - - {item.description.length > 80 - ? item.description.slice(0, 80) + '…' - : item.description} - - )} - - {/* Plugin context menu - shown on hover (not for debug plugins) */} - {isPlugin && !item.debug && ( - handlePluginUpdate(item)} - onDelete={() => handlePluginDelete(item)} - /> - )} - - ); - })} + + ) : item.extensionType === 'plugin' ? ( + + ) : isMCP ? ( + + ) : null} + {item.name} + {item.debug && ( + + )} + + + + {item.description && ( + + {item.description.length > 80 + ? item.description.slice(0, 80) + '…' + : item.description} + + )} + + {/* Plugin context menu - shown on hover (not for debug plugins) */} + {itemIsPluginType && !item.debug && ( + handlePluginUpdate(item)} + onDelete={() => handlePluginDelete(item)} + /> + )} + + ); + }; + + return ( + <> + {showGroupHeaders + ? groupOrder.map((type) => { + const groupItems = visibleItems.filter( + (it) => it.extensionType === type, + ); + if (groupItems.length === 0) return null; + return ( +
+
+ {t(groupLabelKey[type])} +
+ {groupItems.map((item) => renderItem(item))} +
+ ); + }) + : visibleItems.map((item) => renderItem(item))} {/* Show more / less toggle when items exceed limit */} {sortedItems.length > maxItems && !inPopover && ( diff --git a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx index e7478cb8..b18b07e7 100644 --- a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx +++ b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx @@ -26,6 +26,8 @@ export interface SidebarEntityItem { installInfo?: Record; hasUpdate?: boolean; debug?: boolean; + // Set when this item appears in the unified extensions list + extensionType?: 'plugin' | 'mcp' | 'skill'; } // Install action types that can be triggered from sidebar @@ -69,6 +71,9 @@ export interface SidebarDataContextValue { // Pending skill install action triggered from sidebar pendingSkillInstallAction: SkillInstallAction; setPendingSkillInstallAction: (action: SkillInstallAction) => void; + // Whether the extensions list is grouped by type (shared between page and sidebar) + extensionsGroupByType: boolean; + setExtensionsGroupByType: (enabled: boolean) => void; } const SidebarDataContext = createContext(null); @@ -90,6 +95,19 @@ export function SidebarDataProvider({ useState(null); const [pendingSkillInstallAction, setPendingSkillInstallAction] = useState(null); + const [extensionsGroupByType, setExtensionsGroupByTypeState] = + useState(() => { + if (typeof window === 'undefined') return false; + return localStorage.getItem('extensions_group_by_type') === 'true'; + }); + const setExtensionsGroupByType = useCallback((enabled: boolean) => { + setExtensionsGroupByTypeState(enabled); + try { + localStorage.setItem('extensions_group_by_type', String(enabled)); + } catch { + // ignore + } + }, []); const refreshBots = useCallback(async () => { try { @@ -306,6 +324,8 @@ export function SidebarDataProvider({ setPendingPluginInstallAction, pendingSkillInstallAction, setPendingSkillInstallAction, + extensionsGroupByType, + setExtensionsGroupByType, }} > {children} diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 76e31cff..4f42bfc8 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -134,14 +134,8 @@ function PluginListView() { const [copiedDebugUrl, setCopiedDebugUrl] = useState(false); const [copiedDebugKey, setCopiedDebugKey] = useState(false); const [filterType, setFilterType] = useState('all'); - const [groupByType, setGroupByType] = useState(() => { - if (typeof window === 'undefined') return false; - return localStorage.getItem('extensions_group_by_type') === 'true'; - }); - - useEffect(() => { - localStorage.setItem('extensions_group_by_type', String(groupByType)); - }, [groupByType]); + const groupByType = useSidebarData().extensionsGroupByType; + const setGroupByType = useSidebarData().setExtensionsGroupByType; useEffect(() => { const fetchPluginSystemStatus = async () => {