feat(settings): group agents and skills sidebar by subfolder (#464)

* feat(settings): group agents and skills by subfolder in sidebar

- Server: fix getUserAgentPath() to walk subfolders so grouped agent
  layouts (e.g. agents/business/ceo.md) are correctly resolved
- Store: add 'group' field to AgentWithExtras and DiscoveredSkill,
  parsed from file path at load time
- UI: add collapsible SidebarGroup component with localStorage-persisted
  expand/collapse state
- AgentsSidebar: render custom agents grouped by subfolder name
- SkillsSidebar: render project/user skills grouped by domain folder
- Ungrouped items (flat root) fall through and render normally

* fix(settings): normalize group paths and add agent lookup caching parity

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Nguyễn Ngô Thượng
2026-02-23 12:28:24 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 3cd6d051cb
commit b2101acfbf
8 changed files with 453 additions and 33 deletions
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { ButtonLarge } from '@/components/ui/button-large';
import { Input } from '@/components/ui/input';
@@ -25,6 +25,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
import { cn } from '@/lib/utils';
import type { Agent } from '@opencode-ai/sdk/v2';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { SidebarGroup } from '@/components/sections/shared/SidebarGroup';
interface AgentsSidebarProps {
onItemSelect?: () => void;
@@ -307,6 +308,25 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
const builtInAgents = visibleAgents.filter(isAgentBuiltIn);
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
// Group custom agents by subfolder
const { groupedCustomAgents, ungroupedCustomAgents } = useMemo(() => {
const groups: Record<string, typeof customAgents> = {};
const ungrouped: typeof customAgents = [];
for (const agent of customAgents) {
const ext = agent as { group?: string };
if (ext.group) {
if (!groups[ext.group]) groups[ext.group] = [];
groups[ext.group].push(agent);
} else {
ungrouped.push(agent);
}
}
const sortedGroups = Object.keys(groups)
.sort((a, b) => a.localeCompare(b))
.map((name) => ({ name, agents: groups[name] }));
return { groupedCustomAgents: sortedGroups, ungroupedCustomAgents: ungrouped };
}, [customAgents]);
return (
<div className={cn('flex h-full flex-col', bgClass)}>
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
@@ -363,7 +383,38 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
<div className="px-2 pb-1.5 pt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Custom Agents
</div>
{customAgents.map((agent) => (
{/* Grouped agents by subfolder */}
{groupedCustomAgents.map(({ name: groupName, agents: groupAgents }) => (
<SidebarGroup
key={groupName}
label={groupName}
count={groupAgents.length}
storageKey="agents"
>
{groupAgents.map((agent) => (
<AgentListItem
key={agent.name}
agent={agent}
isSelected={selectedAgentName === agent.name}
onSelect={() => {
setSelectedAgent(agent.name);
onItemSelect?.();
if (isMobile) {
setSidebarOpen(false);
}
}}
onRename={() => handleOpenRenameDialog(agent)}
onDelete={() => handleDeleteAgent(agent)}
onDuplicate={() => handleDuplicateAgent(agent)}
getAgentModeIcon={getAgentModeIcon}
/>
))}
</SidebarGroup>
))}
{/* Ungrouped agents (flat in root agents dir) */}
{ungroupedCustomAgents.map((agent) => (
<AgentListItem
key={agent.name}
agent={agent}