diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx
index 12006898..47182b61 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx
@@ -1,4 +1,5 @@
-import Toggle from "@/components/lib/Toggle";
+import { useRef, useEffect } from "react";
+import { SimpleToggleSwitch } from "@/components/lib/Toggle";
export default function SkillRow({
name,
@@ -7,24 +8,30 @@ export default function SkillRow({
highlighted = false,
disabled = false,
}) {
- let classNames = "flex items-center justify-between px-2 py-1 rounded";
+ const ref = useRef(null);
+ useEffect(() => {
+ if (highlighted) ref.current?.scrollIntoView({ block: "nearest" });
+ }, [highlighted]);
+
+ let classNames =
+ "border-none bg-transparent w-full flex items-center justify-between px-2 py-1 rounded";
if (highlighted) classNames += " bg-zinc-700/50 light:bg-slate-100";
else classNames += " hover:bg-zinc-700/50 light:hover:bg-slate-100";
if (disabled) classNames += " opacity-60 cursor-not-allowed";
else classNames += " cursor-pointer";
return (
-
!disabled && onToggle()}
data-tooltip-id={disabled ? "agent-skill-disabled-tooltip" : undefined}
>
{name}
-
-
+
+
+
+
);
}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillSection/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillSection/index.jsx
new file mode 100644
index 00000000..07a1d3b5
--- /dev/null
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillSection/index.jsx
@@ -0,0 +1,57 @@
+import { useRef, useEffect } from "react";
+import { CaretDown } from "@phosphor-icons/react";
+
+export default function SkillSection({
+ name,
+ expanded,
+ onToggle,
+ enabledCount,
+ totalCount,
+ isMcp = false,
+ indented = false,
+ highlighted = false,
+ children,
+}) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (highlighted) ref.current?.scrollIntoView({ block: "nearest" });
+ }, [highlighted]);
+
+ let headerClasses =
+ "border-none bg-transparent w-full flex items-center justify-between px-2 py-1 rounded cursor-pointer";
+ if (highlighted) headerClasses += " bg-zinc-700/50 light:bg-slate-100";
+ else headerClasses += " hover:bg-zinc-700/30 light:hover:bg-slate-50";
+
+ return (
+
+
+ {expanded &&
{children}
}
+
+ );
+}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx
index 5ed32c37..27fb03b9 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx
@@ -1,20 +1,22 @@
-import { useState, useEffect, useMemo } from "react";
+import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import paths from "@/utils/paths";
-import Admin from "@/models/admin";
-import System from "@/models/system";
-import AgentPlugins from "@/models/experimental/agentPlugins";
-import AgentFlows from "@/models/agentFlows";
import {
getDefaultSkills,
getConfigurableSkills,
+ getAppIntegrationSkills,
} from "@/pages/Admin/Agents/skills";
import useToolsMenuItems from "../../useToolsMenuItems";
+import useAgentSkillsState from "./useAgentSkillsState";
+import useSkillSections from "./useSkillSections";
import SkillRow from "./SkillRow";
-import { Wrench } from "@phosphor-icons/react";
+import SkillSection from "./SkillSection";
+import { Wrench, MagnifyingGlass, CircleNotch } from "@phosphor-icons/react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
+const MIN_ITEMS_TO_SHOW_SEARCH = 10;
+
export default function AgentSkillsTab({
highlightedIndex = -1,
registerItemCount,
@@ -23,133 +25,151 @@ export default function AgentSkillsTab({
const { t } = useTranslation();
const { showAgentCommand = true } = workspace ?? {};
const agentSessionActive = useIsAgentSessionActive();
+
+ // Get skill definitions
const defaultSkills = getDefaultSkills(t);
- const [fileSystemAgentAvailable, setFileSystemAgentAvailable] =
- useState(false);
+ const appIntegrationSkills = getAppIntegrationSkills(t);
+
+ // All skill state management
+ const {
+ fileSystemAgentAvailable,
+ importedSkills,
+ flows,
+ mcpServers,
+ loading,
+ mcpLoading,
+ isSkillEnabled,
+ toggleSkill,
+ toggleImportedSkill,
+ toggleFlow,
+ toggleMcpTool,
+ isSubSkillEnabled,
+ toggleSubSkill,
+ disabledSubSkills,
+ } = useAgentSkillsState(defaultSkills);
+
const configurableSkills = getConfigurableSkills(t, {
fileSystemAgentAvailable,
});
- const [disabledDefaults, setDisabledDefaults] = useState([]);
- const [enabledConfigurable, setEnabledConfigurable] = useState([]);
- const [importedSkills, setImportedSkills] = useState([]);
- const [flows, setFlows] = useState([]);
- const [loading, setLoading] = useState(true);
+
+ // UI state
+ const [expandedSections, setExpandedSections] = useState({});
+ const [expandedSubSections, setExpandedSubSections] = useState({});
+ const [searchQuery, setSearchQuery] = useState("");
+
const showAgentCmdActivationAlert = showAgentCommand && !agentSessionActive;
- useEffect(() => {
- fetchSkillSettings();
- }, []);
+ // Build all sections
+ const sections = useSkillSections({
+ t,
+ defaultSkills,
+ configurableSkills,
+ appIntegrationSkills,
+ importedSkills,
+ flows,
+ mcpServers,
+ isSkillEnabled,
+ toggleSkill,
+ isSubSkillEnabled,
+ toggleSubSkill,
+ toggleImportedSkill,
+ toggleFlow,
+ toggleMcpTool,
+ disabledSubSkills,
+ });
- async function fetchSkillSettings() {
- try {
- const [prefs, flowsRes, fsAgentAvailable] = await Promise.all([
- Admin.systemPreferencesByFields([
- "disabled_agent_skills",
- "default_agent_skills",
- "imported_agent_skills",
- ]),
- AgentFlows.listFlows(),
- System.isFileSystemAgentAvailable(),
- ]);
+ // Section expansion helpers
+ function isSectionExpanded(sectionId) {
+ return !!(searchQuery.trim() || expandedSections[sectionId]);
+ }
- if (prefs?.settings) {
- setDisabledDefaults(prefs.settings.disabled_agent_skills ?? []);
- setEnabledConfigurable(prefs.settings.default_agent_skills ?? []);
- setImportedSkills(prefs.settings.imported_agent_skills ?? []);
+ function toggleSection(sectionId) {
+ setExpandedSections((prev) => ({
+ ...prev,
+ [sectionId]: !prev[sectionId],
+ }));
+ }
+
+ function isSubSectionExpanded(subSectionId) {
+ return !!(searchQuery.trim() || expandedSubSections[subSectionId]);
+ }
+
+ function toggleSubSection(subSectionId) {
+ setExpandedSubSections((prev) => ({
+ ...prev,
+ [subSectionId]: !prev[subSectionId],
+ }));
+ }
+
+ // Filter sections by search query
+ const filteredSections = useMemo(() => {
+ if (!searchQuery.trim()) return sections;
+ const q = searchQuery.toLowerCase();
+ return sections
+ .map((section) => {
+ const items = section.items.filter((item) => {
+ const nameMatches = item.name.toLowerCase().includes(q);
+ const subSkillMatches =
+ item.subSkills?.some((sub) => sub.name.toLowerCase().includes(q)) ??
+ false;
+ return nameMatches || subSkillMatches;
+ });
+ return {
+ ...section,
+ items,
+ enabledCount: items.filter((i) => i.enabled).length,
+ };
+ })
+ .filter((section) => section.items.length > 0);
+ }, [sections, searchQuery]);
+
+ // Flat list of navigable items for keyboard nav
+ const { flatItems, flatIndexMap } = useMemo(() => {
+ const items = [];
+ const indexMap = {};
+ for (const section of filteredSections) {
+ indexMap[section.id] = items.length;
+ items.push({
+ type: "header",
+ id: section.id,
+ onToggle: () => toggleSection(section.id),
+ });
+ if (isSectionExpanded(section.id)) {
+ for (const item of section.items) {
+ indexMap[item.id] = items.length;
+ items.push(item);
+
+ if (item.hasSubSkills && item.subSkills) {
+ indexMap[`subsection-${item.id}`] = items.length;
+ items.push({
+ type: "subheader",
+ id: `subsection-${item.id}`,
+ parentId: item.id,
+ onToggle: () => toggleSubSection(item.id),
+ });
+
+ if (isSubSectionExpanded(item.id)) {
+ for (const subItem of item.subSkills) {
+ indexMap[subItem.id] = items.length;
+ items.push(subItem);
+ }
+ }
+ }
+ }
}
- if (flowsRes?.flows) setFlows(flowsRes.flows);
- setFileSystemAgentAvailable(fsAgentAvailable);
- } catch (e) {
- console.error(e);
- } finally {
- setLoading(false);
}
- }
+ return { flatItems: items, flatIndexMap: indexMap };
+ }, [filteredSections, expandedSections, expandedSubSections, searchQuery]);
- function toggleItem(arr, item) {
- return arr.includes(item) ? arr.filter((s) => s !== item) : [...arr, item];
- }
-
- function isSkillEnabled(key) {
- return key in defaultSkills
- ? !disabledDefaults.includes(key)
- : enabledConfigurable.includes(key);
- }
-
- async function toggleSkill(key) {
- if (key in defaultSkills) {
- const updated = toggleItem(disabledDefaults, key);
- setDisabledDefaults(updated);
- await Admin.updateSystemPreferences({
- disabled_agent_skills: updated.join(","),
- default_agent_skills: enabledConfigurable.join(","),
- });
- return;
- }
-
- const updated = toggleItem(enabledConfigurable, key);
- setEnabledConfigurable(updated);
- await Admin.updateSystemPreferences({
- disabled_agent_skills: disabledDefaults.join(","),
- default_agent_skills: updated.join(","),
- });
- }
-
- async function toggleImportedSkill(skill) {
- const newActive = !skill.active;
- setImportedSkills((prev) =>
- prev.map((s) =>
- s.hubId === skill.hubId ? { ...s, active: newActive } : s
- )
- );
- await AgentPlugins.toggleFeature(skill.hubId, newActive);
- }
-
- async function toggleFlow(flow) {
- const newActive = !flow.active;
- setFlows((prev) =>
- prev.map((f) => (f.uuid === flow.uuid ? { ...f, active: newActive } : f))
- );
- await AgentFlows.toggleFlow(flow.uuid, newActive);
- }
-
- // Build list of all skill items for rendering/keyboard navigation
- const items = useMemo(() => {
- const list = [];
- for (const [key, { title }] of Object.entries({
- ...defaultSkills,
- ...configurableSkills,
- })) {
- list.push({
- id: key,
- name: title,
- enabled: isSkillEnabled(key),
- onToggle: () => toggleSkill(key),
- });
- }
- for (const skill of importedSkills) {
- list.push({
- id: skill.hubId,
- name: skill.name,
- enabled: skill.active,
- onToggle: () => toggleImportedSkill(skill),
- });
- }
- for (const flow of flows) {
- list.push({
- id: flow.uuid,
- name: flow.name,
- enabled: flow.active,
- onToggle: () => toggleFlow(flow),
- });
- }
- return list;
- }, [disabledDefaults, enabledConfigurable, importedSkills, flows]);
+ const totalItemCount = sections.reduce((sum, s) => sum + s.items.length, 0);
useToolsMenuItems({
- items,
+ items: flatItems,
highlightedIndex,
- onSelect: agentSessionActive ? () => {} : (item) => item.onToggle(),
+ onSelect: (item) => {
+ if (item.type === "header") return item.onToggle();
+ if (!agentSessionActive) item.onToggle();
+ },
registerItemCount,
});
@@ -162,18 +182,82 @@ export default function AgentSkillsTab({
{t("chat_window.use_agent_session_to_use_tools")}
)}
- {items.map((item, index) => (
- = MIN_ITEMS_TO_SHOW_SEARCH && (
+
+ )}
+ {filteredSections.map((section) => (
+ toggleSection(section.id)}
+ enabledCount={section.enabledCount}
+ totalCount={section.items.length}
+ isMcp={section.isMcp}
+ highlighted={highlightedIndex === flatIndexMap[section.id]}
+ >
+ {section.items.map((item) => (
+
+
+ {item.hasSubSkills && item.subSkills && item.enabled && (
+ toggleSubSection(item.id)}
+ enabledCount={item.subSkills.filter((s) => s.enabled).length}
+ totalCount={item.subSkills.length}
+ highlighted={
+ highlightedIndex === flatIndexMap[`subsection-${item.id}`]
+ }
+ indented
+ >
+ {item.subSkills.map((subItem) => (
+
+ ))}
+
+ )}
+
+ ))}
+
))}
+ {mcpLoading && (
+
+
+
+ {t("chat_window.loading_mcp_servers")}
+
+
+ )}
+ {filteredSections.length === 0 && !mcpLoading && searchQuery.trim() && (
+
+ {t("chat_window.no_tools_found")}
+
+ )}
-