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 <rambat1010@gmail.com>

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield
2026-04-22 17:41:09 -07:00
committed by GitHub
parent 4f3f77119d
commit 55567239b0
35 changed files with 994 additions and 141 deletions

View File

@@ -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 (
<div
<button
ref={ref}
type="button"
className={classNames}
onClick={() => !disabled && onToggle()}
data-tooltip-id={disabled ? "agent-skill-disabled-tooltip" : undefined}
>
<span className="text-xs text-white light:text-slate-900">{name}</span>
<Toggle
size="sm"
enabled={enabled}
onChange={onToggle}
disabled={disabled}
/>
</div>
<div className="pointer-events-none" aria-hidden="true">
<SimpleToggleSwitch size="sm" enabled={enabled} />
</div>
</button>
);
}

View File

@@ -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 (
<div className={indented ? "ml-3" : ""}>
<button
ref={ref}
type="button"
className={headerClasses}
onClick={onToggle}
>
<div className="flex items-center gap-1.5">
<CaretDown
size={10}
weight="bold"
className={`text-zinc-400 light:text-slate-500 transition-transform duration-150 ${
expanded ? "" : "-rotate-90"
}`}
/>
<span className="text-[10px] font-semibold uppercase tracking-wide text-zinc-400 light:text-slate-500">
{name}
</span>
{isMcp && (
<span className="text-[8px] px-1 py-px rounded bg-zinc-600/50 light:bg-slate-200 text-zinc-300 light:text-slate-500 font-medium leading-tight">
MCP
</span>
)}
</div>
<span className="text-[10px] text-zinc-500 light:text-slate-400 tabular-nums">
{enabledCount}/{totalCount}
</span>
</button>
{expanded && <div className="pl-3">{children}</div>}
</div>
);
}

View File

@@ -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")}
</p>
)}
{items.map((item, index) => (
<SkillRow
key={item.id}
name={item.name}
enabled={item.enabled}
onToggle={item.onToggle}
highlighted={highlightedIndex === index}
disabled={agentSessionActive}
{totalItemCount >= MIN_ITEMS_TO_SHOW_SEARCH && (
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
placeholder={t("common.search")}
/>
)}
{filteredSections.map((section) => (
<SkillSection
key={section.id}
name={section.name}
expanded={isSectionExpanded(section.id)}
onToggle={() => toggleSection(section.id)}
enabledCount={section.enabledCount}
totalCount={section.items.length}
isMcp={section.isMcp}
highlighted={highlightedIndex === flatIndexMap[section.id]}
>
{section.items.map((item) => (
<div key={item.id}>
<SkillRow
name={item.name}
enabled={item.enabled}
onToggle={item.onToggle}
highlighted={highlightedIndex === flatIndexMap[item.id]}
disabled={agentSessionActive}
/>
{item.hasSubSkills && item.subSkills && item.enabled && (
<SkillSection
name={t("chat_window.sub_skills")}
expanded={isSubSectionExpanded(item.id)}
onToggle={() => 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) => (
<SkillRow
key={subItem.id}
name={subItem.name}
enabled={subItem.enabled}
onToggle={subItem.onToggle}
highlighted={
highlightedIndex === flatIndexMap[subItem.id]
}
disabled={agentSessionActive || !subItem.parentEnabled}
/>
))}
</SkillSection>
)}
</div>
))}
</SkillSection>
))}
{mcpLoading && (
<div className="flex items-center gap-1.5 px-2 py-1.5">
<CircleNotch
size={12}
className="text-zinc-500 light:text-slate-400 animate-spin"
weight="bold"
/>
<span className="text-[10px] text-zinc-500 light:text-slate-400">
{t("chat_window.loading_mcp_servers")}
</span>
</div>
)}
{filteredSections.length === 0 && !mcpLoading && searchQuery.trim() && (
<p className="text-xs text-zinc-500 light:text-slate-400 text-center py-2">
{t("chat_window.no_tools_found")}
</p>
)}
<Link to={paths.settings.agentSkills()}>
<button className="flex items-center gap-1.5 px-2 h-6 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 text-theme-text-primary">
<button className="border-none flex items-center gap-1.5 px-2 h-6 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 text-theme-text-primary">
<Wrench size={12} className="text-theme-text-primary" />
<span className="text-xs text-theme-text-primary">
{t("chat_window.manage_agent_skills")}
@@ -183,3 +267,30 @@ export default function AgentSkillsTab({
</>
);
}
function SearchInput({ value, onChange, placeholder }) {
return (
<div className="relative shrink-0">
<MagnifyingGlass
size={12}
className="absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400 light:text-slate-400"
weight="bold"
/>
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Escape") {
onChange("");
e.target.blur();
}
if (e.key === "Enter") e.preventDefault();
}}
className="w-full pl-7 pr-2 py-1 text-xs bg-zinc-700/50 light:bg-slate-100 border border-zinc-600 light:border-slate-300 rounded text-white light:text-slate-900 placeholder:text-zinc-500 light:placeholder:text-slate-400 outline-none focus:border-zinc-500 light:focus:border-slate-400"
/>
</div>
);
}

View File

@@ -0,0 +1,81 @@
import { getCreateFileSkills } from "@/pages/Admin/Agents/CreateFileSkillPanel";
import { getFileSystemSubSkills } from "@/pages/Admin/Agents/FileSystemSkillPanel";
import { getGmailSkills } from "@/pages/Admin/Agents/GMailSkillPanel/utils";
import { getGoogleCalendarSkills } from "@/pages/Admin/Agents/GoogleCalendarSkillPanel/utils";
import { getOutlookSkills } from "@/pages/Admin/Agents/OutlookSkillPanel/utils";
/**
* Flattens categorized skills (used by app integrations) into a flat array.
*/
function flattenCategorySkills(categorizedSkills) {
return Object.values(categorizedSkills).flatMap(
(category) => category.skills
);
}
/**
* Registry of all skills that have sub-skills.
* Each entry maps a skill key to its configuration:
* - preferenceKey: The system preference key for storing disabled sub-skills
* - getSubSkills: Function that returns the sub-skills array (receives translation function)
*
* To add a new skill with sub-skills:
* 1. Add an entry here with the skill key, preference key, and getter function
* 2. The rest is handled automatically by useSubSkillPreferences hook
*/
export const SUB_SKILL_REGISTRY = {
"create-files-agent": {
preferenceKey: "disabled_create_files_skills",
getSubSkills: (t) => getCreateFileSkills(t),
},
"filesystem-agent": {
preferenceKey: "disabled_filesystem_skills",
getSubSkills: (t) => getFileSystemSubSkills(t),
},
"gmail-agent": {
preferenceKey: "disabled_gmail_skills",
getSubSkills: (t) => flattenCategorySkills(getGmailSkills(t)),
},
"google-calendar-agent": {
preferenceKey: "disabled_google_calendar_skills",
getSubSkills: (t) => flattenCategorySkills(getGoogleCalendarSkills(t)),
},
"outlook-agent": {
preferenceKey: "disabled_outlook_skills",
getSubSkills: (t) => flattenCategorySkills(getOutlookSkills(t)),
},
};
/**
* Get all preference keys that need to be fetched for sub-skills.
*/
export function getSubSkillPreferenceKeys() {
return Object.values(SUB_SKILL_REGISTRY).map(
(config) => config.preferenceKey
);
}
/**
* Get sub-skills for a given skill key.
* Returns null if the skill has no sub-skills.
*/
export function getSubSkillsForSkill(skillKey, t) {
const config = SUB_SKILL_REGISTRY[skillKey];
if (!config) return null;
return config.getSubSkills(t);
}
/**
* Get the preference key for a skill's sub-skills.
* Returns null if the skill has no sub-skills.
*/
export function getPreferenceKeyForSkill(skillKey) {
return SUB_SKILL_REGISTRY[skillKey]?.preferenceKey ?? null;
}
/**
* Check if a skill has sub-skills.
*/
export function hasSubSkills(skillKey) {
return skillKey in SUB_SKILL_REGISTRY;
}

View File

@@ -0,0 +1,182 @@
import { useState, useEffect, useCallback } from "react";
import Admin from "@/models/admin";
import System from "@/models/system";
import AgentPlugins from "@/models/experimental/agentPlugins";
import AgentFlows from "@/models/agentFlows";
import MCPServers from "@/models/mcpServers";
import { getSubSkillPreferenceKeys } from "./skillRegistry";
import useSubSkillPreferences from "./useSubSkillPreferences";
/**
* Core hook for managing all agent skill state.
* Handles fetching, toggling, and persisting skill preferences.
*/
export default function useAgentSkillsState(defaultSkills) {
// Core skill state
const [fileSystemAgentAvailable, setFileSystemAgentAvailable] =
useState(false);
const [disabledDefaults, setDisabledDefaults] = useState([]);
const [enabledConfigurable, setEnabledConfigurable] = useState([]);
const [importedSkills, setImportedSkills] = useState([]);
const [flows, setFlows] = useState([]);
const [mcpServers, setMcpServers] = useState([]);
const [loading, setLoading] = useState(true);
const [mcpLoading, setMcpLoading] = useState(true);
// Sub-skill preferences (managed by dedicated hook)
const subSkillPrefs = useSubSkillPreferences();
// Fetch all skill settings on mount
useEffect(() => {
fetchSkillSettings();
fetchMcpServers();
}, []);
async function fetchSkillSettings() {
try {
const subSkillPrefKeys = getSubSkillPreferenceKeys();
const [prefs, flowsRes, fsAgentAvailable] = await Promise.all([
Admin.systemPreferencesByFields([
"disabled_agent_skills",
"default_agent_skills",
"imported_agent_skills",
...subSkillPrefKeys,
]),
AgentFlows.listFlows(),
System.isFileSystemAgentAvailable(),
]);
if (prefs?.settings) {
setDisabledDefaults(prefs.settings.disabled_agent_skills ?? []);
setEnabledConfigurable(prefs.settings.default_agent_skills ?? []);
setImportedSkills(prefs.settings.imported_agent_skills ?? []);
subSkillPrefs.loadFromSettings(prefs.settings);
}
if (flowsRes?.flows) setFlows(flowsRes.flows);
setFileSystemAgentAvailable(fsAgentAvailable);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
async function fetchMcpServers() {
try {
const { servers = [] } = await MCPServers.listServers();
setMcpServers(servers);
} catch (e) {
console.error(e);
} finally {
setMcpLoading(false);
}
}
// Skill enabled/disabled checks
const isSkillEnabled = useCallback(
(key) => {
return key in defaultSkills
? !disabledDefaults.includes(key)
: enabledConfigurable.includes(key);
},
[defaultSkills, disabledDefaults, enabledConfigurable]
);
// Toggle functions
const toggleSkill = useCallback(
async (key) => {
const toggleItem = (arr, item) =>
arr.includes(item) ? arr.filter((s) => s !== item) : [...arr, item];
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(","),
});
},
[defaultSkills, disabledDefaults, enabledConfigurable]
);
const toggleImportedSkill = useCallback(async (skill) => {
const newActive = !skill.active;
setImportedSkills((prev) =>
prev.map((s) =>
s.hubId === skill.hubId ? { ...s, active: newActive } : s
)
);
await AgentPlugins.toggleFeature(skill.hubId, newActive);
}, []);
const toggleFlow = useCallback(async (flow) => {
const newActive = !flow.active;
setFlows((prev) =>
prev.map((f) => (f.uuid === flow.uuid ? { ...f, active: newActive } : f))
);
await AgentFlows.toggleFlow(flow.uuid, newActive);
}, []);
const toggleMcpTool = useCallback(
async (serverName, toolName, currentlyEnabled) => {
const newEnabled = !currentlyEnabled;
setMcpServers((prev) => {
return prev.map((server) => {
if (server.name !== serverName) return server;
const currentSuppressed =
server.config?.anythingllm?.suppressedTools || [];
const newSuppressed = newEnabled
? currentSuppressed.filter((t) => t !== toolName)
: [...currentSuppressed, toolName];
return {
...server,
config: {
...server.config,
anythingllm: {
...server.config?.anythingllm,
suppressedTools: newSuppressed,
},
},
};
});
});
await MCPServers.toggleTool(serverName, toolName, newEnabled);
},
[]
);
return {
// State
fileSystemAgentAvailable,
disabledDefaults,
enabledConfigurable,
importedSkills,
flows,
mcpServers,
loading,
mcpLoading,
// Skill checks
isSkillEnabled,
// Toggle functions
toggleSkill,
toggleImportedSkill,
toggleFlow,
toggleMcpTool,
// Sub-skill preferences (delegated)
isSubSkillEnabled: subSkillPrefs.isSubSkillEnabled,
toggleSubSkill: subSkillPrefs.toggleSubSkill,
disabledSubSkills: subSkillPrefs.disabledSubSkills,
};
}

View File

@@ -0,0 +1,187 @@
import { useMemo } from "react";
import { titleCase } from "text-case";
import { getSubSkillsForSkill, hasSubSkills } from "./skillRegistry";
/**
* Builds a skill item with optional sub-skills.
*/
function buildSkillItem({
key,
title,
isEnabled,
onToggle,
t,
isSubSkillEnabled,
toggleSubSkill,
}) {
const subSkills = getSubSkillsForSkill(key, t);
const parentEnabled = isEnabled(key);
return {
id: key,
name: title,
enabled: parentEnabled,
onToggle: () => onToggle(key),
hasSubSkills: hasSubSkills(key),
subSkills: subSkills
? subSkills.map((sub) => ({
id: `${key}::${sub.name}`,
name: sub.title,
enabled: parentEnabled && isSubSkillEnabled(key, sub.name),
onToggle: () => toggleSubSkill(key, sub.name),
parentEnabled,
}))
: null,
};
}
/**
* Hook to build all skill sections for the menu.
* Separates the section-building logic from the main component.
*/
export default function useSkillSections({
t,
defaultSkills,
configurableSkills,
appIntegrationSkills,
importedSkills,
flows,
mcpServers,
isSkillEnabled,
toggleSkill,
isSubSkillEnabled,
toggleSubSkill,
toggleImportedSkill,
toggleFlow,
toggleMcpTool,
disabledSubSkills,
}) {
return useMemo(() => {
const sectionList = [];
// Agent Skills (default + configurable)
const skillItems = [];
for (const [key, { title }] of Object.entries({
...defaultSkills,
...configurableSkills,
})) {
skillItems.push(
buildSkillItem({
key,
title,
isEnabled: isSkillEnabled,
onToggle: toggleSkill,
t,
isSubSkillEnabled,
toggleSubSkill,
})
);
}
if (skillItems.length > 0) {
sectionList.push({
id: "agent-skills",
name: t("chat_window.agent_skills"),
items: skillItems,
enabledCount: skillItems.filter((i) => i.enabled).length,
});
}
// App Integrations
const appIntegrationItems = [];
for (const [key, { title }] of Object.entries(appIntegrationSkills)) {
appIntegrationItems.push(
buildSkillItem({
key,
title,
isEnabled: isSkillEnabled,
onToggle: toggleSkill,
t,
isSubSkillEnabled,
toggleSubSkill,
})
);
}
if (appIntegrationItems.length > 0) {
sectionList.push({
id: "app-integrations",
name: t("chat_window.app_integrations"),
items: appIntegrationItems,
enabledCount: appIntegrationItems.filter((i) => i.enabled).length,
});
}
// Custom Skills (imported)
if (importedSkills.length > 0) {
const items = importedSkills.map((skill) => ({
id: skill.hubId,
name: skill.name,
enabled: skill.active,
onToggle: () => toggleImportedSkill(skill),
}));
sectionList.push({
id: "custom-skills",
name: t("chat_window.custom_skills"),
items,
enabledCount: items.filter((i) => i.enabled).length,
});
}
// Agent Flows
if (flows.length > 0) {
const items = flows.map((flow) => ({
id: flow.uuid,
name: flow.name,
enabled: flow.active,
onToggle: () => toggleFlow(flow),
}));
sectionList.push({
id: "agent-flows",
name: t("chat_window.agent_flows"),
items,
enabledCount: items.filter((i) => i.enabled).length,
});
}
// MCP Servers
for (const server of mcpServers) {
if (!server.running || server.tools.length === 0) continue;
const suppressedTools = server.config?.anythingllm?.suppressedTools || [];
const items = server.tools.map((tool) => ({
id: `mcp::${server.name}::${tool.name}`,
name: tool.name,
enabled: !suppressedTools.includes(tool.name),
onToggle: () =>
toggleMcpTool(
server.name,
tool.name,
!suppressedTools.includes(tool.name)
),
}));
sectionList.push({
id: `mcp-${server.name}`,
name: titleCase(server.name.replace(/[_-]/g, " ")),
isMcp: true,
items,
enabledCount: items.filter((i) => i.enabled).length,
});
}
return sectionList;
}, [
t,
defaultSkills,
configurableSkills,
appIntegrationSkills,
importedSkills,
flows,
mcpServers,
isSkillEnabled,
toggleSkill,
isSubSkillEnabled,
toggleSubSkill,
toggleImportedSkill,
toggleFlow,
toggleMcpTool,
disabledSubSkills,
]);
}

View File

@@ -0,0 +1,78 @@
import { useState, useCallback } from "react";
import Admin from "@/models/admin";
import { SUB_SKILL_REGISTRY, getPreferenceKeyForSkill } from "./skillRegistry";
/**
* Hook to manage sub-skill preferences for all skills in the registry.
* Handles loading, checking enabled state, and toggling sub-skills.
*
* This hook eliminates the need for separate state variables for each skill's
* sub-skills. Adding a new skill with sub-skills only requires updating the
* skillRegistry.js file.
*/
export default function useSubSkillPreferences() {
// Single state object holding disabled sub-skills for all skills
// Key: preferenceKey, Value: array of disabled sub-skill names
const [disabledSubSkills, setDisabledSubSkills] = useState({});
/**
* Load sub-skill preferences from settings object.
* Called after fetching system preferences.
*/
const loadFromSettings = useCallback((settings) => {
if (!settings) return;
const loaded = {};
for (const [, config] of Object.entries(SUB_SKILL_REGISTRY)) {
const value = settings[config.preferenceKey];
loaded[config.preferenceKey] = value ?? [];
}
setDisabledSubSkills(loaded);
}, []);
/**
* Check if a sub-skill is enabled for a given skill.
*/
const isSubSkillEnabled = useCallback(
(skillKey, subSkillName) => {
const prefKey = getPreferenceKeyForSkill(skillKey);
if (!prefKey) return true;
const disabled = disabledSubSkills[prefKey] ?? [];
return !disabled.includes(subSkillName);
},
[disabledSubSkills]
);
/**
* Toggle a sub-skill's enabled state.
*/
const toggleSubSkill = useCallback(
async (skillKey, subSkillName) => {
const prefKey = getPreferenceKeyForSkill(skillKey);
if (!prefKey) return;
const current = disabledSubSkills[prefKey] ?? [];
const updated = current.includes(subSkillName)
? current.filter((s) => s !== subSkillName)
: [...current, subSkillName];
setDisabledSubSkills((prev) => ({
...prev,
[prefKey]: updated,
}));
await Admin.updateSystemPreferences({
[prefKey]: updated.join(","),
});
},
[disabledSubSkills]
);
return {
loadFromSettings,
isSubSkillEnabled,
toggleSubSkill,
disabledSubSkills,
};
}

View File

@@ -142,7 +142,7 @@ export default function ToolsMenu({
))}
</div>
<div className="flex flex-col gap-1 overflow-y-auto no-scroll flex-1 min-h-0">
<div className="flex flex-col gap-1 overflow-y-auto no-scroll min-h-0">
<ActiveTab
sendCommand={sendCommand}
setShowing={setShowing}

View File

@@ -1174,6 +1174,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "تمت الموافقة على طلب الحصول على الأدوات.",
tool_call_was_rejected: "تم رفض طلب الاتصال بالأداة.",
},
custom_skills: "المهارات المخصصة",
agent_flows: "تدفقات الوكلاء",
no_tools_found: "لم يتم العثور على أدوات مطابقة.",
loading_mcp_servers: "تحميل خوادم MCP...",
app_integrations: "تكامل التطبيقات",
sub_skills: "مهارات فرعية",
},
profile_settings: {
edit_account: "تحرير الحساب",

View File

@@ -1399,6 +1399,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "La crida a l'eina ha estat aprovada",
tool_call_was_rejected: "La crida a l'eina ha estat rebutjada",
},
custom_skills: "Habilitats personalitzades",
agent_flows: "Fluxos d'agents",
no_tools_found: "No s'han trobat eines corresponents.",
loading_mcp_servers: "Carregant servidors MCP...",
app_integrations: "Integracions d'aplicacions",
sub_skills: "Habilitats específiques",
},
profile_settings: {
edit_account: "Edita el compte",

View File

@@ -1308,6 +1308,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "Žádost o použití nástroje byla schválena.",
tool_call_was_rejected: "Žádost o použití nástroje byla zamítnuta.",
},
custom_skills: "Vlastní dovednosti",
agent_flows: "Toky agentů",
no_tools_found: "Nebyla nalezena žádná odpovídající nářadí.",
loading_mcp_servers: "Načítají se servery pro MCP...",
app_integrations: "Integrace aplikací",
sub_skills: "Specifické dovednosti",
},
profile_settings: {
edit_account: "Upravit účet",

View File

@@ -1188,6 +1188,12 @@ const TRANSLATIONS = {
"Anmodningen om at bruge værktøjet blev godkendt.",
tool_call_was_rejected: "Anmodningen om at bruge værktøjet blev afvist.",
},
custom_skills: "Skræddersyede færdigheder",
agent_flows: "Agentstrømme",
no_tools_found: "Ingen matchende værktøjer fundet",
loading_mcp_servers: "Indlæser MCP-servere...",
app_integrations: "App-integrationer",
sub_skills: "Specifikke færdigheder",
},
profile_settings: {
edit_account: "Rediger konto",

View File

@@ -1304,6 +1304,12 @@ const TRANSLATIONS = {
"Die Genehmigung für die Bestellung der Werkzeuge wurde erteilt.",
tool_call_was_rejected: "Die Anfrage nach dem Werkzeug wurde abgelehnt.",
},
custom_skills: "Individuelle Fähigkeiten",
agent_flows: "Datenströme",
no_tools_found: "Keine passenden Werkzeuge gefunden.",
loading_mcp_servers: "MCP-Server laden...",
app_integrations: "Anwendungen und Integrationen",
sub_skills: "Spezifische Fähigkeiten",
},
profile_settings: {
edit_account: "Account bearbeiten",

View File

@@ -1336,6 +1336,12 @@ const TRANSLATIONS = {
slash_commands: "Slash Commands",
agent_skills: "Agent Skills",
manage_agent_skills: "Manage Agent Skills",
app_integrations: "App Integrations",
custom_skills: "Custom Skills",
agent_flows: "Agent Flows",
sub_skills: "Sub-skills",
no_tools_found: "No matching tools found",
loading_mcp_servers: "Loading MCP servers...",
start_agent_session: "Start Agent Session",
agent_skills_disabled_in_session:
"Can't modify skills during an active agent session. Use /exit to end the session first.",

View File

@@ -1320,6 +1320,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "La solicitud de herramientas ha sido aprobada.",
tool_call_was_rejected: "La solicitud de herramienta fue rechazada.",
},
custom_skills: "Habilidades personalizadas",
agent_flows: "Flujos de agentes",
no_tools_found: "No se encontraron herramientas coincidentes.",
loading_mcp_servers: "Cargando servidores de MCP...",
app_integrations: "Integraciones de aplicaciones",
sub_skills: "Habilidades específicas",
},
profile_settings: {
edit_account: "Editar cuenta",

View File

@@ -1247,6 +1247,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "Vahendite tellimuse kinnitati.",
tool_call_was_rejected: "Vahendite taotlus jäeti rahuldamata.",
},
custom_skills: "Kohandatud oskused",
agent_flows: "Agentide liiklus",
no_tools_found: "Välja ei leitud sobivaid tööriistu",
loading_mcp_servers: "MCP-serverite laadimine...",
app_integrations: "Rakenduste integreerimine",
sub_skills: "Alamspetsid",
},
profile_settings: {
edit_account: "Muuda kontot",

View File

@@ -1180,6 +1180,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "درخواست برای تهیه ابزار تأیید شد.",
tool_call_was_rejected: "درخواست استفاده از ابزار رد شد.",
},
custom_skills: "مهارت‌های تخصصی",
agent_flows: "جریان‌های نمایندگی",
no_tools_found: "هیچ ابزار مشابهی یافت نشد.",
loading_mcp_servers: "بارگذاری سرورهای MCP...",
app_integrations: "ادغام با برنامه‌ها",
sub_skills: "مهارت‌های پایه",
},
profile_settings: {
edit_account: "ویرایش حساب",

View File

@@ -1212,6 +1212,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"La demande d'utilisation de l'outil a été rejetée.",
},
custom_skills: "Compétences spécifiques",
agent_flows: "Flux des agents",
no_tools_found: "Aucun outil correspondant n'a été trouvé.",
loading_mcp_servers: "Chargement des serveurs MCP...",
app_integrations: "Intégrations d'applications",
sub_skills: "Compétences spécifiques",
},
profile_settings: {
edit_account: "Modifier le compte",

View File

@@ -1236,6 +1236,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "הבקשה לקבלת הכלי אושרה.",
tool_call_was_rejected: "בקשת השימוש בכלי נדחתה.",
},
custom_skills: "כישורים מותאמים אישית",
agent_flows: "זרימת סוכנים",
no_tools_found: "לא נמצאו כלים תואמים.",
loading_mcp_servers: "טעינת שרתי ה-MCP...",
app_integrations: "אינטגרציות עם אפליקציות",
sub_skills: "כישורים ספציפיים",
},
profile_settings: {
edit_account: "ערוך חשבון",

View File

@@ -1217,6 +1217,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"La richiesta di accesso all'attrezzatura è stata rifiutata.",
},
custom_skills: "Competenze personalizzate",
agent_flows: "Flussi di agenti",
no_tools_found: "Nessuno strumento corrispondente trovato.",
loading_mcp_servers: "Inizio caricamento dei server MCP...",
app_integrations: "Integrazioni di applicazioni",
sub_skills: "Competenze specifiche",
},
profile_settings: {
edit_account: "Modifica account",

View File

@@ -1168,6 +1168,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "ツールの使用許可が承認されました",
tool_call_was_rejected: "ツール呼び出しは拒否されました",
},
custom_skills: "カスタマイズ可能なスキル",
agent_flows: "エージェント間の流れ",
no_tools_found: "一致するツールは見つかりませんでした",
loading_mcp_servers: "MCP サーバーの読み込み中...",
app_integrations: "アプリケーション連携",
sub_skills: "専門スキル",
},
profile_settings: {
edit_account: "アカウントを編集",

View File

@@ -1249,6 +1249,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "도구 사용 승인",
tool_call_was_rejected: "도구 호출이 거부되었습니다.",
},
custom_skills: "맞춤형 기술",
agent_flows: "에이전트 흐름",
no_tools_found: "일치하는 도구가 없습니다.",
loading_mcp_servers: "MCP 서버 로딩 중...",
app_integrations: "앱 통합",
sub_skills: "세부 기술",
},
profile_settings: {
edit_account: "계정 정보 수정",

View File

@@ -1312,6 +1312,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "Įrankių užsakymas buvo patvirtintas.",
tool_call_was_rejected: "Klausimas dėl įrankio buvo atmetamas.",
},
custom_skills: "Individualūs įgūdžiai",
agent_flows: "Agentų srautai",
no_tools_found: "Nėra rasti atitikusių įrankių.",
loading_mcp_servers: "Įkrauname MCP serverius...",
app_integrations: "Programų integracijos",
sub_skills: "Pagrindinės įgūdžios",
},
profile_settings: {
edit_account: "Redaguoti paskyrą",

View File

@@ -1293,6 +1293,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"Pieprasījums par instrumenta izmantošanu tika atgrūstīts.",
},
custom_skills: "Pielāgotas prasmes",
agent_flows: "Aģentu plūsmas",
no_tools_found: "Neatradusies atbilstošas instrumentus",
loading_mcp_servers: "Ielāde MCP serverus...",
app_integrations: "Dienvidligzdas integrācijas",
sub_skills: "Īpašās prasmes",
},
profile_settings: {
edit_account: "Rediģēt kontu",

View File

@@ -1196,6 +1196,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"De aanvraag om het gereedschap te gebruiken is afgewezen.",
},
custom_skills: "Aangepaste vaardigheden",
agent_flows: "Stroom van agenten",
no_tools_found: "Geen overeenkomende gereedschappen gevonden.",
loading_mcp_servers: "MCP-servers worden geladen...",
app_integrations: "Integraties met apps",
sub_skills: "Specifieke vaardigheden",
},
profile_settings: {
edit_account: "Account bewerken",

View File

@@ -1297,6 +1297,12 @@ const TRANSLATIONS = {
"Zgłoszenie dotyczące narzędzia zostało zatwierdzone.",
tool_call_was_rejected: "Żądanie użycia narzędzia zostało odrzucone.",
},
custom_skills: "Dostosowane umiejętności",
agent_flows: "Przepływy agencji",
no_tools_found: "Nie znaleziono odpowiadających narzędzi.",
loading_mcp_servers: "Ładowanie serwerów MCP...",
app_integrations: "Integracje z aplikacjami",
sub_skills: "Specyficzne umiejętności",
},
profile_settings: {
edit_account: "Edytuj konto",

View File

@@ -1280,6 +1280,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"A solicitação de acesso à ferramenta foi rejeitada.",
},
custom_skills: "Habilidades personalizadas",
agent_flows: "Fluxo de Agentes",
no_tools_found: "Nenhuma ferramenta correspondente encontrada.",
loading_mcp_servers: "Carregando servidores MCP...",
app_integrations: "Integrações de aplicativos",
sub_skills: "Habilidades específicas",
},
profile_settings: {
edit_account: "Editar conta",

View File

@@ -577,6 +577,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"Cererea de utilizare a instrumentului a fost respinsă.",
},
custom_skills: "Abilități personalizate",
agent_flows: "Fluxuri de agenți",
no_tools_found: "Nu au fost găsite instrumente corespunzătoare.",
loading_mcp_servers: "Încărcare servere MCP...",
app_integrations: "Integrarea aplicațiilor",
sub_skills: "Abilități specifice",
},
profile_settings: {
edit_account: "Editează contul",

View File

@@ -1206,6 +1206,12 @@ const TRANSLATIONS = {
tool_call_was_rejected:
"Запрос на предоставление инструмента был отклонен.",
},
custom_skills: "Индивидуальные навыки",
agent_flows: "Поток агентов",
no_tools_found: "Не найдено соответствующих инструментов.",
loading_mcp_servers: "Загрузка серверов MCP...",
app_integrations: "Интеграция с приложениями",
sub_skills: "Подквалификация",
},
profile_settings: {
edit_account: "Редактировать учётную запись",

View File

@@ -1200,6 +1200,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "Araç talebi onaylandı.",
tool_call_was_rejected: "Ara çağrısı reddedildi.",
},
custom_skills: "Özel Beceri",
agent_flows: "Ajans Akışları",
no_tools_found: "Uyumlu herhangi bir araç bulunamadı",
loading_mcp_servers: "MCP sunucularının yüklenmesi...",
app_integrations: "Uygulama Entegrasyonları",
sub_skills: "Alt beceriler",
},
profile_settings: {
edit_account: "Hesabı Düzenle",

View File

@@ -1184,6 +1184,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "Đã được phê duyệt yêu cầu dụng cụ.",
tool_call_was_rejected: "Yêu cầu gọi công cụ đã bị từ chối.",
},
custom_skills: "Kỹ năng tùy chỉnh",
agent_flows: "Dòng chảy của đại lý",
no_tools_found: "Không tìm thấy công cụ tương ứng.",
loading_mcp_servers: "Đang tải các máy chủ MCP...",
app_integrations: "Tích hợp ứng dụng",
sub_skills: "Kỹ năng chuyên môn",
},
profile_settings: {
edit_account: "Chỉnh sửa Tài khoản",

View File

@@ -1195,6 +1195,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "工具使用申请已获得批准。",
tool_call_was_rejected: "请求获取工具已被拒绝。",
},
custom_skills: "定制技能",
agent_flows: "代理人流动",
no_tools_found: "未找到匹配的工具",
loading_mcp_servers: "正在加载 MCP 服务器…",
app_integrations: "应用程序集成",
sub_skills: "基本技能",
},
profile_settings: {
edit_account: "编辑帐户",

View File

@@ -1108,6 +1108,12 @@ const TRANSLATIONS = {
tool_call_was_approved: "工具請求已獲得批准。",
tool_call_was_rejected: "請求已遭拒絕",
},
custom_skills: "客製化技能",
agent_flows: "代理人流",
no_tools_found: "未找到匹配的工具",
loading_mcp_servers: "正在載入 MCP 伺服器...",
app_integrations: "應用程式整合",
sub_skills: "細項技能",
},
profile_settings: {
edit_account: "編輯帳戶",

View File

@@ -11,7 +11,7 @@ import {
} from "@phosphor-icons/react";
import Admin from "@/models/admin";
const getCreateFileSkills = (t) => [
export const getCreateFileSkills = (t) => [
{
name: "create-text-file",
title: t("agent.skill.createFiles.skills.create-text-file.title"),

View File

@@ -18,7 +18,7 @@ import {
} from "@phosphor-icons/react";
import Admin from "@/models/admin";
const getFileSystemSubSkills = (t) => {
export const getFileSystemSubSkills = (t) => {
return [
{
name: "filesystem-read-text-file",