From 55567239b0ba48b60b20330736600e31dcbaf163 Mon Sep 17 00:00:00 2001
From: Sean Hatfield
Date: Wed, 22 Apr 2026 17:41:09 -0700
Subject: [PATCH] Show agent skills, flows, and MCP tools in chat tools menu
(#5444)
* show agent skills, flows, and MCP tools in collapsible sections in chat tools menu
* fix tools menu toggle disabled bypass, add border-none to buttons, and useMemo improvements
* replace mcp server cache with loading state for mcp servers
* enable sub-skill management
* refactor
* Translations for chat tools menu improvements (#5448)
* normalize translations
* update translations
* norm translations
---------
Co-authored-by: Timothy Carambat
---------
Co-authored-by: Timothy Carambat
---
.../Tabs/AgentSkills/SkillRow/index.jsx | 27 +-
.../Tabs/AgentSkills/SkillSection/index.jsx | 57 +++
.../ToolsMenu/Tabs/AgentSkills/index.jsx | 367 ++++++++++++------
.../Tabs/AgentSkills/skillRegistry.js | 81 ++++
.../Tabs/AgentSkills/useAgentSkillsState.js | 182 +++++++++
.../Tabs/AgentSkills/useSkillSections.js | 187 +++++++++
.../AgentSkills/useSubSkillPreferences.js | 78 ++++
.../PromptInput/ToolsMenu/index.jsx | 2 +-
frontend/src/locales/ar/common.js | 6 +
frontend/src/locales/ca/common.js | 6 +
frontend/src/locales/cs/common.js | 6 +
frontend/src/locales/da/common.js | 6 +
frontend/src/locales/de/common.js | 6 +
frontend/src/locales/en/common.js | 6 +
frontend/src/locales/es/common.js | 6 +
frontend/src/locales/et/common.js | 6 +
frontend/src/locales/fa/common.js | 6 +
frontend/src/locales/fr/common.js | 6 +
frontend/src/locales/he/common.js | 6 +
frontend/src/locales/it/common.js | 6 +
frontend/src/locales/ja/common.js | 6 +
frontend/src/locales/ko/common.js | 6 +
frontend/src/locales/lt/common.js | 6 +
frontend/src/locales/lv/common.js | 6 +
frontend/src/locales/nl/common.js | 6 +
frontend/src/locales/pl/common.js | 6 +
frontend/src/locales/pt_BR/common.js | 6 +
frontend/src/locales/ro/common.js | 6 +
frontend/src/locales/ru/common.js | 6 +
frontend/src/locales/tr/common.js | 6 +
frontend/src/locales/vn/common.js | 6 +
frontend/src/locales/zh/common.js | 6 +
frontend/src/locales/zh_TW/common.js | 6 +
.../Agents/CreateFileSkillPanel/index.jsx | 2 +-
.../Agents/FileSystemSkillPanel/index.jsx | 2 +-
35 files changed, 994 insertions(+), 141 deletions(-)
create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillSection/index.jsx
create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/skillRegistry.js
create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/useAgentSkillsState.js
create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/useSkillSections.js
create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/useSubSkillPreferences.js
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")}
+
+ )}
-