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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
3cd6d051cb
commit
b2101acfbf
@@ -81,8 +81,26 @@ export type AgentWithExtras = Agent & {
|
||||
native?: boolean;
|
||||
hidden?: boolean;
|
||||
options?: { hidden?: boolean };
|
||||
scope?: AgentScope;
|
||||
/** Subfolder name parsed from file path, e.g. "business", "development" */
|
||||
group?: string;
|
||||
};
|
||||
|
||||
/** Parse the subfolder group name from an agent file path.
|
||||
* e.g. "~/.config/opencode/agents/business/ceo.md" → "business"
|
||||
* e.g. "~/.config/opencode/agents/ceo.md" → undefined
|
||||
*/
|
||||
function parseAgentGroup(path: string | null | undefined): string | undefined {
|
||||
if (!path) return undefined;
|
||||
const normalizedPath = path.replace(/\\/g, '/');
|
||||
const idx = normalizedPath.lastIndexOf('/agents/');
|
||||
if (idx === -1) return undefined;
|
||||
const relative = normalizedPath.substring(idx + '/agents/'.length);
|
||||
const parts = relative.split('/');
|
||||
// parts[0] = group, parts[1] = filename; need at least 2 parts
|
||||
return parts.length > 1 ? parts[0] : undefined;
|
||||
}
|
||||
|
||||
// Helper to check if agent is built-in (handles both SDK 'builtIn' and API 'native')
|
||||
export const isAgentBuiltIn = (agent: Agent): boolean => {
|
||||
const extended = agent as AgentWithExtras & { builtIn?: boolean };
|
||||
@@ -202,12 +220,16 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
?? sources.json?.scope;
|
||||
}
|
||||
|
||||
// Parse subfolder group from file path
|
||||
const mdPath: string | null | undefined = data.sources?.md?.path;
|
||||
const group = parseAgentGroup(mdPath);
|
||||
|
||||
if (scope === 'project' || scope === 'user') {
|
||||
return { ...agent, scope: scope as AgentScope };
|
||||
return { ...agent, scope: scope as AgentScope, group };
|
||||
}
|
||||
|
||||
// Explicitly set null scope if not found, to clear stale state
|
||||
return { ...agent, scope: undefined };
|
||||
return { ...agent, scope: undefined, group };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[AgentsStore] Failed to fetch config for agent ${agent.name}:`, err);
|
||||
|
||||
@@ -64,6 +64,23 @@ export interface DiscoveredSkill {
|
||||
scope: SkillScope;
|
||||
source: SkillSource;
|
||||
description?: string;
|
||||
/** Domain folder parsed from file path, e.g. "automation-ai", "lark-ecosystem" */
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/** Parse the domain group folder from a skill file path.
|
||||
* e.g. "~/.config/opencode/skills/automation-ai/ai-production/SKILL.md" → "automation-ai"
|
||||
* e.g. "~/.config/opencode/skills/theme-system/SKILL.md" → undefined (flat)
|
||||
*/
|
||||
function parseSkillGroup(path: string): string | undefined {
|
||||
const normalizedPath = path.replace(/\\/g, '/');
|
||||
const idx = normalizedPath.lastIndexOf('/skills/');
|
||||
if (idx === -1) return undefined;
|
||||
const relative = normalizedPath.substring(idx + '/skills/'.length);
|
||||
const parts = relative.split('/');
|
||||
// Grouped layout: <group>/<name>/SKILL.md → parts.length >= 3
|
||||
// Flat layout: <name>/SKILL.md → parts.length == 2
|
||||
return parts.length >= 3 ? parts[0] : undefined;
|
||||
}
|
||||
|
||||
// Raw skill response from API before transformation
|
||||
@@ -185,6 +202,7 @@ export const useSkillsStore = create<SkillsStore>()(
|
||||
scope: s.scope ?? 'user',
|
||||
source: s.source ?? 'opencode',
|
||||
description: s.sources?.md?.description || '',
|
||||
group: parseSkillGroup(s.path),
|
||||
}));
|
||||
|
||||
set({ skills, isLoading: false });
|
||||
|
||||
Reference in New Issue
Block a user