diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index cdc330a9..60bd93a1 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -92,7 +92,7 @@ import { } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; import { useSidebarData, SidebarEntityItem } from './SidebarDataContext'; -import { LayoutDashboard } from 'lucide-react'; +import { LayoutDashboard, Puzzle } from 'lucide-react'; // Compare two version strings, returns true if v1 > v2 function compareVersions(v1: string, v2: string): boolean { @@ -1040,7 +1040,7 @@ function PluginItemMenu({ ); } -// Plugin pages navigation section +// Plugin pages navigation section — grouped by plugin function PluginPagesNav() { const { pluginPages } = useSidebarData(); const navigate = useNavigate(); @@ -1054,27 +1054,87 @@ function PluginPagesNav() { const currentId = pathname === '/home/plugin-pages' ? searchParams.get('id') : null; + // Group pages by plugin (author/name) + const grouped = new Map< + string, + { label: string; pages: typeof pluginPages } + >(); + for (const page of pluginPages) { + const key = `${page.pluginAuthor}/${page.pluginName}`; + if (!grouped.has(key)) { + grouped.set(key, { label: page.pluginLabel, pages: [] }); + } + grouped.get(key)!.pages.push(page); + } + return ( - {t('sidebar.pluginPages')} + + {t('sidebar.pluginPages')} + - {pluginPages.map((page) => { - const isActive = currentId === page.id; - const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`; - return ( - - navigate(route)} + {Array.from(grouped.entries()).map( + ([pluginKey, { label, pages }]) => { + const hasActivePage = pages.some((p) => p.id === currentId); + + // Single page — render directly without nesting + if (pages.length === 1) { + const page = pages[0]; + const isActive = currentId === page.id; + const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`; + return ( + + navigate(route)} + > + + {page.name} + + + ); + } + + // Multiple pages — collapsible group + return ( + - - {page.name} - - - ); - })} + + + + + {label} + + + + + + {pages.map((page) => { + const isActive = currentId === page.id; + const route = `/home/plugin-pages?id=${encodeURIComponent(page.id)}`; + return ( + + navigate(route)} + > + {page.name} + + + ); + })} + + + + + ); + }, + )} diff --git a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx index fb917f89..95fe10c0 100644 --- a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx +++ b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx @@ -37,6 +37,7 @@ export interface PluginPageItem { name: string; // display label pluginAuthor: string; pluginName: string; + pluginLabel: string; // human-readable plugin display name pageId: string; path: string; // asset path (HTML file) icon?: string; // optional icon name @@ -191,6 +192,7 @@ export function SidebarDataProvider({ const meta = plugin.manifest.manifest.metadata; const author = meta.author ?? ''; const name = meta.name; + const label = meta.label ? extractI18nObject(meta.label) : name; const spec = plugin.manifest.manifest.spec; if (spec?.pages && Array.isArray(spec.pages)) { for (const page of spec.pages) { @@ -202,6 +204,7 @@ export function SidebarDataProvider({ name: page.label ? extractI18nObject(page.label) : page.id, pluginAuthor: author, pluginName: name, + pluginLabel: label, pageId: page.id, path: page.path, icon: page.icon, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 5880117c..b82ba13b 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -5,7 +5,8 @@ const enUS = { installedPlugins: 'Installed Plugins', pluginMarket: 'Marketplace', mcpServers: 'MCP Servers', - pluginPages: 'Extension Pages', + pluginPages: 'Plugin Pages', + pluginPagesTooltip: 'Visual pages provided by installed plugins', quickStart: 'Quick Start', }, common: { @@ -1294,8 +1295,8 @@ const enUS = { }, }, pluginPages: { - selectFromSidebar: 'Select an extension page from the sidebar', - invalidPage: 'Invalid extension page', + selectFromSidebar: 'Select a plugin page from the sidebar', + invalidPage: 'Invalid plugin page', }, }; diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 1cac589c..8641428b 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -5,7 +5,8 @@ const zhHans = { installedPlugins: '已安装插件', pluginMarket: '插件市场', mcpServers: 'MCP 服务器', - pluginPages: '扩展页', + pluginPages: '插件页面', + pluginPagesTooltip: '由已安装的插件提供的可视化页面', quickStart: '快速开始向导', }, common: { @@ -1236,8 +1237,8 @@ const zhHans = { }, }, pluginPages: { - selectFromSidebar: '从侧边栏选择一个扩展页', - invalidPage: '无效的扩展页', + selectFromSidebar: '从侧边栏选择一个插件页面', + invalidPage: '无效的插件页面', }, };