feat(sidebar): add group-by-kind toggle to Agent section

Add a toggle (left of the "+" button) that groups the Agent section by
kind, showing "Agent" and "Pipeline" sub-headers. State persists in
localStorage, mirroring the extensions group-by-type pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Junyan Qin
2026-06-26 20:17:50 +08:00
parent 89221b59ed
commit 957396b6e2
5 changed files with 76 additions and 1 deletions
@@ -34,6 +34,7 @@ import {
RefreshCcw, RefreshCcw,
Bot, Bot,
Workflow, Workflow,
Group,
} from 'lucide-react'; } from 'lucide-react';
import { useTheme } from '@/components/providers/theme-provider'; import { useTheme } from '@/components/providers/theme-provider';
@@ -509,6 +510,7 @@ function NavItems({
const isSkill = categoryId === 'skills'; const isSkill = categoryId === 'skills';
const isBot = categoryId === 'bots'; const isBot = categoryId === 'bots';
const isMCP = categoryId === 'mcp'; const isMCP = categoryId === 'mcp';
const isAgents = categoryId === 'pipelines';
const resolveItemRoute = (item: SidebarEntityItem): string => { const resolveItemRoute = (item: SidebarEntityItem): string => {
if (item.extensionType === 'mcp') { if (item.extensionType === 'mcp') {
@@ -591,6 +593,18 @@ function NavItems({
!inPopover && !inPopover &&
sidebarData.extensionsGroupByType; sidebarData.extensionsGroupByType;
const showAgentGroupHeaders =
isAgents && !inPopover && sidebarData.agentsGroupByKind;
const agentGroupOrder: Array<'agent' | 'pipeline'> = [
'agent',
'pipeline',
];
const agentGroupLabelKey: Record<'agent' | 'pipeline', string> = {
agent: 'agents.kindBadgeAgent',
pipeline: 'agents.kindBadgePipeline',
};
const groupOrder: Array<'plugin' | 'mcp' | 'skill'> = [ const groupOrder: Array<'plugin' | 'mcp' | 'skill'> = [
'plugin', 'plugin',
'mcp', 'mcp',
@@ -792,7 +806,25 @@ function NavItems({
</div> </div>
); );
}) })
: visibleItems.map((item) => renderItem(item))} : showAgentGroupHeaders
? agentGroupOrder.map((kind) => {
const groupItems = visibleItems.filter(
(it) => (it.kind ?? 'agent') === kind,
);
if (groupItems.length === 0) return null;
return (
<div
key={kind}
className="flex flex-col gap-0.5 mt-0.5"
>
<div className="px-2 pt-1 pb-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground">
{t(agentGroupLabelKey[kind])}
</div>
{groupItems.map((item) => renderItem(item))}
</div>
);
})
: visibleItems.map((item) => renderItem(item))}
{/* Show more / less toggle when items exceed limit */} {/* Show more / less toggle when items exceed limit */}
{sortedItems.length > maxItems && !inPopover && ( {sortedItems.length > maxItems && !inPopover && (
<SidebarMenuSubItem> <SidebarMenuSubItem>
@@ -1030,6 +1062,26 @@ function NavItems({
{config.name} {config.name}
</span> </span>
<div className="ml-auto flex items-center gap-0.5 -mr-1"> <div className="ml-auto flex items-center gap-0.5 -mr-1">
{isAgents && (
<button
type="button"
title={t('agents.groupByKind')}
className={cn(
'p-1 rounded-sm transition-all',
sidebarData.agentsGroupByKind
? 'text-sidebar-accent-foreground bg-sidebar-accent'
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100',
)}
onClick={(e) => {
e.stopPropagation();
sidebarData.setAgentsGroupByKind(
!sidebarData.agentsGroupByKind,
);
}}
>
<Group className="size-3.5" />
</button>
)}
{isExtensionsCategory && ( {isExtensionsCategory && (
<button <button
type="button" type="button"
@@ -66,6 +66,9 @@ export interface SidebarDataContextValue {
// Whether the extensions list is grouped by type (shared between page and sidebar) // Whether the extensions list is grouped by type (shared between page and sidebar)
extensionsGroupByType: boolean; extensionsGroupByType: boolean;
setExtensionsGroupByType: (enabled: boolean) => void; setExtensionsGroupByType: (enabled: boolean) => void;
// Whether the Agent list is grouped by kind (Agent vs Pipeline)
agentsGroupByKind: boolean;
setAgentsGroupByKind: (enabled: boolean) => void;
} }
const SidebarDataContext = createContext<SidebarDataContextValue | null>(null); const SidebarDataContext = createContext<SidebarDataContextValue | null>(null);
@@ -97,6 +100,21 @@ export function SidebarDataProvider({
} }
}, []); }, []);
const [agentsGroupByKind, setAgentsGroupByKindState] = useState<boolean>(
() => {
if (typeof window === 'undefined') return false;
return localStorage.getItem('agents_group_by_kind') === 'true';
},
);
const setAgentsGroupByKind = useCallback((enabled: boolean) => {
setAgentsGroupByKindState(enabled);
try {
localStorage.setItem('agents_group_by_kind', String(enabled));
} catch {
// ignore
}
}, []);
const refreshBots = useCallback(async () => { const refreshBots = useCallback(async () => {
try { try {
const resp = await httpClient.getBots(); const resp = await httpClient.getBots();
@@ -311,6 +329,8 @@ export function SidebarDataProvider({
setDetailEntityName, setDetailEntityName,
extensionsGroupByType, extensionsGroupByType,
setExtensionsGroupByType, setExtensionsGroupByType,
agentsGroupByKind,
setAgentsGroupByKind,
}} }}
> >
{children} {children}
+1
View File
@@ -519,6 +519,7 @@ const enUS = {
pipelineType: 'Pipeline', pipelineType: 'Pipeline',
kindBadgeAgent: 'Agent', kindBadgeAgent: 'Agent',
kindBadgePipeline: 'Pipeline', kindBadgePipeline: 'Pipeline',
groupByKind: 'Group by type',
pipelineTypeDescription: pipelineTypeDescription:
'Keep the existing no-code message pipeline for backward compatibility. It only handles message events.', 'Keep the existing no-code message pipeline for backward compatibility. It only handles message events.',
allEvents: 'Supports all EBA events', allEvents: 'Supports all EBA events',
+1
View File
@@ -503,6 +503,7 @@ const jaJP = {
pipelineType: 'Pipeline', pipelineType: 'Pipeline',
kindBadgeAgent: 'Agent', kindBadgeAgent: 'Agent',
kindBadgePipeline: 'パイプライン', kindBadgePipeline: 'パイプライン',
groupByKind: 'タイプ別にグループ化',
pipelineTypeDescription: pipelineTypeDescription:
'既存のノーコードメッセージ Pipeline を互換性のため保持します。メッセージイベントのみ処理できます。', '既存のノーコードメッセージ Pipeline を互換性のため保持します。メッセージイベントのみ処理できます。',
allEvents: 'すべての EBA イベントに対応', allEvents: 'すべての EBA イベントに対応',
+1
View File
@@ -499,6 +499,7 @@ const zhHans = {
pipelineType: 'Pipeline', pipelineType: 'Pipeline',
kindBadgeAgent: 'Agent', kindBadgeAgent: 'Agent',
kindBadgePipeline: '流水线', kindBadgePipeline: '流水线',
groupByKind: '按类型分组',
pipelineTypeDescription: pipelineTypeDescription:
'保留现有无代码消息流水线,兼容旧配置,只能处理消息事件。', '保留现有无代码消息流水线,兼容旧配置,只能处理消息事件。',
allEvents: '支持全部 EBA 事件', allEvents: '支持全部 EBA 事件',