diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js
index 3f06de59ee..b862dd1dc3 100644
--- a/api/server/controllers/agents/client.js
+++ b/api/server/controllers/agents/client.js
@@ -429,6 +429,8 @@ class AgentClient extends BaseClient {
subagents: agent.subagents,
memory_scope: agent.memory_scope,
skills_enabled: agent.skills_enabled,
+ skill_authoring_enabled: agent.skill_authoring_enabled,
+ skills_scope: agent.skills_scope,
skills: agent.skills,
backgroundToolNames: agent.backgroundToolNames,
intentToolNames: agent.intentToolNames,
diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js
index f412b152ff..d7b155fec6 100644
--- a/api/server/controllers/agents/v1.js
+++ b/api/server/controllers/agents/v1.js
@@ -41,6 +41,7 @@ const {
const {
Time,
Tools,
+ SkillsScope,
CacheKeys,
Constants,
FileSources,
@@ -241,18 +242,38 @@ const blockFilteredAgentContent = async (req, res, agentData) => {
const sanitizeViewerSkillScope = (agent, accessibleSkillSet) => {
const skillScopeEnabled = agent.skills_enabled === true;
+ const configuredScope = agent.skills_scope;
delete agent.skills_enabled;
+ delete agent.skills_scope;
if (!skillScopeEnabled) {
delete agent.skills;
return agent;
}
- const configuredSkills = Array.isArray(agent.skills) ? agent.skills : [];
- if (configuredSkills.length === 0) {
- // Empty allowlist means the viewer's full accessible catalog.
+ if (configuredScope === SkillsScope.none) {
delete agent.skills;
agent.skills_enabled = true;
+ agent.skills_scope = SkillsScope.none;
+ return agent;
+ }
+
+ if (configuredScope === SkillsScope.all) {
+ delete agent.skills;
+ agent.skills_enabled = true;
+ agent.skills_scope = SkillsScope.all;
+ return agent;
+ }
+
+ const configuredSkills = Array.isArray(agent.skills) ? agent.skills : [];
+ if (configuredSkills.length === 0) {
+ // Legacy empty allowlists mean the viewer's full accessible catalog;
+ // explicit selected scope remains an intentionally empty catalog.
+ delete agent.skills;
+ agent.skills_enabled = true;
+ if (configuredScope === SkillsScope.selected) {
+ agent.skills_scope = SkillsScope.selected;
+ }
return agent;
}
@@ -262,11 +283,18 @@ const sanitizeViewerSkillScope = (agent, accessibleSkillSet) => {
if (visibleSkills.length === 0) {
delete agent.skills;
+ if (configuredScope === SkillsScope.selected) {
+ agent.skills_enabled = true;
+ agent.skills_scope = SkillsScope.selected;
+ }
return agent;
}
agent.skills = visibleSkills;
agent.skills_enabled = true;
+ if (configuredScope === SkillsScope.selected) {
+ agent.skills_scope = SkillsScope.selected;
+ }
return agent;
};
diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js
index ef1345586f..daef3a4981 100644
--- a/api/server/controllers/agents/v1.spec.js
+++ b/api/server/controllers/agents/v1.spec.js
@@ -4,6 +4,7 @@ const { v4: uuidv4 } = require('uuid');
const { agentSchema, aclEntrySchema, fileSchema, userSchema } = require('@librechat/data-schemas');
const {
Tools,
+ SkillsScope,
FileSources,
PermissionBits,
PrincipalModel,
@@ -3210,6 +3211,34 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(response.data[0].skills_enabled).toBe(true);
});
+ test('should preserve an enabled empty catalog for VIEW list callers', async () => {
+ await Agent.findByIdAndUpdate(agentA1._id, {
+ skills_enabled: false,
+ skill_authoring_enabled: true,
+ skills_scope: SkillsScope.none,
+ skills: [],
+ });
+
+ mockReq.user.id = userB.toString();
+ mockReq.query.requiredPermission = String(PermissionBits.VIEW);
+ findAccessibleResources.mockImplementation(({ resourceType }) => {
+ if (resourceType === ResourceType.AGENT) {
+ return Promise.resolve([agentA1._id]);
+ }
+ return Promise.resolve([]);
+ });
+ findPubliclyAccessibleResources.mockResolvedValue([]);
+
+ await getListAgentsHandler(mockReq, mockRes);
+
+ const response = mockRes.json.mock.calls[0][0];
+ expect(response.data).toHaveLength(1);
+ expect(response.data[0].skills).toBeUndefined();
+ expect(response.data[0].skills_enabled).toBeUndefined();
+ expect(response.data[0].skill_authoring_enabled).toBe(true);
+ expect(response.data[0].skills_scope).toBeUndefined();
+ });
+
test('should return raw skill configuration for EDIT list callers', async () => {
const visibleSkillId = new mongoose.Types.ObjectId();
const hiddenSkillId = new mongoose.Types.ObjectId();
diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js
index d23bde9089..cb80c3807b 100644
--- a/api/server/services/Endpoints/agents/initialize.spec.js
+++ b/api/server/services/Endpoints/agents/initialize.spec.js
@@ -551,6 +551,41 @@ describe('initializeClient — processAgent ACL gate', () => {
expect(initializeParams.skillAuthoringAvailable).toBe(true);
});
+ it('enables standalone authoring without exposing the persisted agent skill catalog', async () => {
+ const endpointOption = makeEndpointOption();
+ endpointOption.agent = Promise.resolve({
+ id: PRIMARY_ID,
+ name: 'Primary',
+ provider: 'openai',
+ model: 'gpt-4',
+ tools: [],
+ skills: [],
+ skills_enabled: false,
+ skill_authoring_enabled: true,
+ });
+ mockInitializeAgent.mockResolvedValue(makePrimaryConfig([]));
+ const req = makeReq();
+ req.config.endpoints.agents = { capabilities: ['skills'] };
+ const canCreateSkillSpy = jest
+ .spyOn(getSkillToolDeps(), 'canCreateSkill')
+ .mockResolvedValue(true);
+
+ try {
+ await initializeClient({
+ req,
+ res: {},
+ signal: new AbortController().signal,
+ endpointOption,
+ });
+ } finally {
+ canCreateSkillSpy.mockRestore();
+ }
+
+ const initializeParams = mockInitializeAgent.mock.calls[0][0];
+ expect(initializeParams.accessibleSkillIds).toEqual([]);
+ expect(initializeParams.skillAuthoringAvailable).toBe(true);
+ });
+
it('loads model validation and skill permissions without serial waits', async () => {
const models = deferred();
const createPermission = deferred();
diff --git a/api/server/services/Endpoints/agents/skillDeps.js b/api/server/services/Endpoints/agents/skillDeps.js
index 485e37f198..b58983da9a 100644
--- a/api/server/services/Endpoints/agents/skillDeps.js
+++ b/api/server/services/Endpoints/agents/skillDeps.js
@@ -158,7 +158,11 @@ function canEditSkill({ req, skillId }) {
});
}
-function isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) {
+function isAgentSkillAuthoringEnabledForRun({
+ agent,
+ skillsCapabilityEnabled,
+ ephemeralSkillsToggle,
+}) {
if (!skillsCapabilityEnabled) {
return false;
}
@@ -171,7 +175,7 @@ function isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralS
}
return ephemeralSkillsToggle === true;
}
- return agent.skills_enabled === true;
+ return agent.skills_enabled === true || agent.skill_authoring_enabled === true;
}
function canAuthorSkillFiles({
@@ -182,7 +186,11 @@ function canAuthorSkillFiles({
ephemeralSkillsToggle,
}) {
return (
- isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) &&
+ isAgentSkillAuthoringEnabledForRun({
+ agent,
+ skillsCapabilityEnabled,
+ ephemeralSkillsToggle,
+ }) &&
(scopedEditableSkillIds.length > 0 || skillCreateAllowed === true)
);
}
@@ -397,7 +405,7 @@ function getSkillToolDeps() {
module.exports = {
getSkillToolDeps,
canAuthorSkillFiles,
- isAgentSkillsEnabledForRun,
+ isAgentSkillAuthoringEnabledForRun,
getSkillDbMethods,
withDeploymentSkillIds,
getSkillStrategyFunctions,
diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts
index 1500d9e503..9808f4c973 100644
--- a/client/src/common/agents-types.ts
+++ b/client/src/common/agents-types.ts
@@ -6,6 +6,7 @@ import type {
SupportContact,
AgentProvider,
MemoryScope,
+ SkillsScope,
StatefulCodeEnvironment,
GraphEdge,
Agent,
@@ -43,6 +44,8 @@ export type AgentForm = {
tool_options?: AgentToolOptions;
skills?: string[];
skills_enabled?: boolean;
+ skill_authoring_enabled?: boolean;
+ skills_scope?: SkillsScope;
/** Memory partition: 'agent' isolates memories per (user, agent); default shared pool */
memory_scope?: MemoryScope;
/** Sharing scope for stateful Code API workspaces. */
diff --git a/client/src/components/Chat/Input/SkillsCommand.tsx b/client/src/components/Chat/Input/SkillsCommand.tsx
index b04c6b2e7e..27d5ad91e9 100644
--- a/client/src/components/Chat/Input/SkillsCommand.tsx
+++ b/client/src/components/Chat/Input/SkillsCommand.tsx
@@ -1,13 +1,16 @@
import { memo, useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { ScrollText } from 'lucide-react';
+import { useSetRecoilState } from 'recoil';
+import { useAtomValue, useSetAtom } from 'jotai';
import { AutoSizer, List } from 'react-virtualized';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Input, Spinner, useCombobox } from '@librechat/client';
+import { SkillsScope, resolveAgentSkillsScope } from 'librechat-data-provider';
import type { TSkillSummary } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import useInitPopoverInput from '~/hooks/Input/useInitPopoverInput';
import { useLocalize, useSkillActiveState } from '~/hooks';
import { useSkillsInfiniteQuery } from '~/data-provider';
+import { showSkillsPopoverFamily } from './skillsState';
import { useAgentsMapContext } from '~/Providers';
import { ephemeralAgentByConvoId } from '~/store';
import { isEphemeralAgent } from '~/common';
@@ -86,7 +89,7 @@ function SkillsCommandContent({
agentId?: string | null;
}) {
const localize = useLocalize();
- const setShowSkillsPopover = useSetRecoilState(store.showSkillsPopoverFamily(index));
+ const setShowSkillsPopover = useSetAtom(showSkillsPopoverFamily(index));
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(conversationId));
const setPendingManualSkills = useSetRecoilState(
store.pendingManualSkillsByConvoId(conversationId),
@@ -99,9 +102,8 @@ function SkillsCommandContent({
`resolveAgentScopedSkillIds`: ephemeral agents always see the full
catalog (picking any skill flips `ephemeralAgent.skills = true` and
activates for the turn); persisted agents gate on the builder's
- `skills_enabled` master toggle — off or unset means opt-out, on with
- an empty allowlist means full catalog, on with a non-empty allowlist
- means narrow to those ids. Persisted agents fail closed during the
+ `skills_enabled` master toggle and explicit catalog scope. Legacy agents
+ without a scope retain empty allowlist = full catalog. Persisted agents fail closed during the
`agentsMap` hydration window so the user cannot pick a skill the
backend will then refuse, and again when the map is authoritative
but the agent isn't in it (deleted, or VIEW revoked mid-session).
@@ -121,7 +123,14 @@ function SkillsCommandContent({
if (agent.skills_enabled !== true) {
return [];
}
- return Array.isArray(agent.skills) && agent.skills.length > 0 ? agent.skills : undefined;
+ const scope = resolveAgentSkillsScope(agent.skills, agent.skills_enabled, agent.skills_scope);
+ if (scope === SkillsScope.none) {
+ return [];
+ }
+ if (scope === SkillsScope.all) {
+ return undefined;
+ }
+ return agent.skills ?? [];
}, [agentId, agentsMap]);
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
@@ -375,7 +384,7 @@ const SkillsCommand = memo(function SkillsCommand({
conversationId: string;
agentId?: string | null;
}) {
- const show = useRecoilValue(store.showSkillsPopoverFamily(index));
+ const show = useAtomValue(showSkillsPopoverFamily(index));
if (!show) {
return null;
}
diff --git a/client/src/components/Chat/Input/__tests__/SkillsCommand.spec.tsx b/client/src/components/Chat/Input/__tests__/SkillsCommand.spec.tsx
index e032206950..6cc2a17fa6 100644
--- a/client/src/components/Chat/Input/__tests__/SkillsCommand.spec.tsx
+++ b/client/src/components/Chat/Input/__tests__/SkillsCommand.spec.tsx
@@ -13,6 +13,7 @@
import React from 'react';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { SkillsScope } from 'librechat-data-provider';
import { render, screen } from '@testing-library/react';
import type { TSkillSummary } from 'librechat-data-provider';
@@ -23,21 +24,27 @@ const mockSetEphemeralAgent = jest.fn();
const mockSetPendingManualSkills = jest.fn();
const mockShowSkillsPopover = { current: true };
+jest.mock('jotai', () => ({
+ ...jest.requireActual('jotai'),
+ useAtomValue: jest.fn((atom: unknown) =>
+ atom === 'show-skills-popover' ? mockShowSkillsPopover.current : undefined,
+ ),
+ useSetAtom: jest.fn((atom: unknown) =>
+ atom === 'show-skills-popover' ? mockSetShowSkillsPopover : jest.fn(),
+ ),
+}));
+
+jest.mock('../skillsState', () => ({
+ showSkillsPopoverFamily: () => 'show-skills-popover',
+}));
+
jest.mock('recoil', () => {
const actual = jest.requireActual('recoil');
return {
...actual,
- useRecoilValue: jest.fn((atom: unknown) => {
- if (atom === 'show-skills-popover') {
- return mockShowSkillsPopover.current;
- }
- return undefined;
- }),
+ useRecoilValue: jest.fn(() => undefined),
useRecoilState: jest.fn(() => [null, jest.fn()]),
useSetRecoilState: jest.fn((atom: unknown) => {
- if (atom === 'show-skills-popover') {
- return mockSetShowSkillsPopover;
- }
if (atom === 'ephemeral-agent') {
return mockSetEphemeralAgent;
}
@@ -52,7 +59,6 @@ jest.mock('recoil', () => {
jest.mock('~/store', () => ({
__esModule: true,
default: {
- showSkillsPopoverFamily: () => 'show-skills-popover',
pendingManualSkillsByConvoId: () => 'pending-manual-skills',
},
ephemeralAgentByConvoId: () => 'ephemeral-agent',
@@ -379,6 +385,39 @@ describe('SkillsCommand', () => {
expect(screen.getByRole('button', { name: /Style Guide/i })).toBeInTheDocument();
});
+ it('shows no catalog entries for an enabled authoring-only agent', () => {
+ mockUseSkillsInfiniteQuery.mockReturnValue({
+ data: twoSkillsResponse,
+ isLoading: false,
+ isError: false,
+ fetchNextPage: jest.fn(),
+ hasNextPage: false,
+ isFetchingNextPage: false,
+ });
+ mockUseAgentsMapContext.mockReturnValue({
+ agent_1: {
+ id: 'agent_1',
+ skills: [],
+ skills_enabled: false,
+ skill_authoring_enabled: true,
+ skills_scope: SkillsScope.none,
+ },
+ });
+
+ const textAreaRef = makeTextarea('$');
+ render(
+ ,
+ );
+
+ expect(screen.queryByRole('button', { name: /Brand Guidelines/i })).toBeNull();
+ expect(screen.queryByRole('button', { name: /Style Guide/i })).toBeNull();
+ });
+
it('treats an ephemeral agent id as unscoped and shows the full ACL catalog', async () => {
mockUseSkillsInfiniteQuery.mockReturnValue({
data: twoSkillsResponse,
diff --git a/client/src/components/Chat/Input/skillsState.ts b/client/src/components/Chat/Input/skillsState.ts
new file mode 100644
index 0000000000..a7982a498f
--- /dev/null
+++ b/client/src/components/Chat/Input/skillsState.ts
@@ -0,0 +1,4 @@
+import { atom } from 'jotai';
+import { atomFamily } from 'jotai/utils';
+
+export const showSkillsPopoverFamily = atomFamily((_index: string | number | null) => atom(false));
diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx
index 63a9b593fa..3ea8cfc854 100644
--- a/client/src/components/SidePanel/Agents/AgentPanel.tsx
+++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx
@@ -91,6 +91,8 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
tool_options,
skills,
skills_enabled,
+ skill_authoring_enabled,
+ skills_scope,
memory_scope,
avatar_action: avatarActionState,
} = data;
@@ -132,6 +134,8 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
tool_options: normalizedToolOptions,
skills,
skills_enabled,
+ skill_authoring_enabled,
+ skills_scope,
/** A hidden stale 'agent' scope must not survive disabling memory —
* runtime partitioning keys off memory_scope alone. */
memory_scope: data.memory === true ? memory_scope : MemoryScope.user,
diff --git a/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx b/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
index e57b503247..dbcdc906bc 100644
--- a/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
@@ -19,7 +19,7 @@ import type { AgentItem } from './items/types';
import type { AgentForm } from '~/common';
import { useLocalize, useHasAccess, useAuthContext, useToolFavorites } from '~/hooks';
import { CreateSkillDialog } from '~/components/Skills/dialogs';
-import { skillsEnabledTransition } from './items/mutations';
+import { skillsSelectionTransition } from './items/mutations';
import { useSkillsInfiniteQuery } from '~/data-provider';
import MarketplaceCatalog from './MarketplaceCatalog';
import { CategoryIcon } from '~/components/Prompts';
@@ -143,9 +143,20 @@ export default function SkillsDialog({ open, onOpenChange, agentId }: SkillsDial
const applySkillsSelection = useCallback(
(next: string[]) => {
setValue('skills', next, { shouldDirty: true });
- const flag = skillsEnabledTransition(next, getValues('skills_enabled'));
- if (flag !== undefined) {
- setValue('skills_enabled', flag, { shouldDirty: true });
+ const transition = skillsSelectionTransition(
+ next,
+ getValues('skills_enabled'),
+ getValues('skill_authoring_enabled'),
+ getValues('skills_scope'),
+ );
+ if (transition.enabled !== undefined) {
+ setValue('skills_enabled', transition.enabled, { shouldDirty: true });
+ }
+ if (transition.authoringEnabled !== undefined) {
+ setValue('skill_authoring_enabled', transition.authoringEnabled, { shouldDirty: true });
+ }
+ if (transition.scope !== undefined) {
+ setValue('skills_scope', transition.scope, { shouldDirty: true });
}
},
[getValues, setValue],
diff --git a/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx b/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
index 4f06069d00..4e7634a5d8 100644
--- a/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
@@ -1,12 +1,6 @@
import { useState, useMemo, useCallback, useRef } from 'react';
import { Plus } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
-import {
- PermissionTypes,
- Permissions,
- AgentCapabilities,
- removeCodeExecutionCaller,
-} from 'librechat-data-provider';
import {
Label,
Switch,
@@ -17,6 +11,14 @@ import {
OGDialogTemplate,
useToastContext,
} from '@librechat/client';
+import {
+ PermissionTypes,
+ Permissions,
+ SkillsScope,
+ AgentCapabilities,
+ resolveAgentSkillsScope,
+ removeCodeExecutionCaller,
+} from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import type { CapabilityFileCounts } from './items/capabilities';
@@ -28,7 +30,7 @@ import {
useResolvedSkills,
useUninstallToolCredentials,
} from './hooks';
-import { computeToggleAction, skillsEnabledTransition } from './items/mutations';
+import { computeToggleAction, skillsSelectionTransition } from './items/mutations';
import { useListSkillsQuery, useDeleteAgentAction } from '~/data-provider';
import { requiresFileManagerRemoval } from './items/capabilities';
import { useRemoveMCPTool, useVisibleTools } from '~/hooks/MCP';
@@ -90,7 +92,16 @@ export default function ToolsSection({ agentId }: Props) {
const skillsValue = useWatch({ control, name: 'skills' });
const skillsEnabledValue = useWatch({ control, name: 'skills_enabled' });
- const useAllSkills = skillsEnabledValue === true && (skillsValue ?? []).length === 0;
+ const skillAuthoringEnabledValue = useWatch({ control, name: 'skill_authoring_enabled' });
+ const skillsScopeValue = useWatch({ control, name: 'skills_scope' });
+ const explicitSkillsEnabled = skillsEnabledValue === true;
+ const skillsCapabilityEnabled = explicitSkillsEnabled || skillAuthoringEnabledValue === true;
+ const effectiveSkillsScope = resolveAgentSkillsScope(
+ skillsValue,
+ skillsEnabledValue,
+ skillsScopeValue,
+ );
+ const useAllSkills = explicitSkillsEnabled && effectiveSkillsScope === SkillsScope.all;
/** Selection stashed when "use all skills" turns on, so turning it back off
* restores the previous picks instead of destroying them. Cleared when the
* agent changes — the section isn't remounted on switch (only the form
@@ -103,17 +114,65 @@ export default function ToolsSection({ agentId }: Props) {
stashedSkillsRef.current = [];
}
+ const handleSkillsEnabledChange = useCallback(
+ (checked: boolean) => {
+ if (!checked) {
+ setValue('skills_enabled', false, { shouldDirty: true });
+ setValue('skill_authoring_enabled', false, { shouldDirty: true });
+ return;
+ }
+ const current = (getValues('skills') ?? []) as string[];
+ if (current.length === 0 && getValues('skills_scope') === SkillsScope.all) {
+ setValue('skills_enabled', true, { shouldDirty: true });
+ setValue('skill_authoring_enabled', false, { shouldDirty: true });
+ return;
+ }
+ const transition = skillsSelectionTransition(
+ current,
+ getValues('skills_enabled'),
+ getValues('skill_authoring_enabled'),
+ getValues('skills_scope'),
+ );
+ if (transition.enabled !== undefined) {
+ setValue('skills_enabled', transition.enabled, { shouldDirty: true });
+ }
+ if (transition.authoringEnabled !== undefined) {
+ setValue('skill_authoring_enabled', transition.authoringEnabled, { shouldDirty: true });
+ }
+ if (transition.scope !== undefined) {
+ setValue('skills_scope', transition.scope, { shouldDirty: true });
+ }
+ },
+ [getValues, setValue],
+ );
+
const handleUseAllSkillsChange = useCallback(
(checked: boolean) => {
if (checked) {
stashedSkillsRef.current = (getValues('skills') ?? []) as string[];
setValue('skills', [], { shouldDirty: true });
setValue('skills_enabled', true, { shouldDirty: true });
+ setValue('skill_authoring_enabled', false, { shouldDirty: true });
+ setValue('skills_scope', SkillsScope.all, { shouldDirty: true });
return;
}
const restored = stashedSkillsRef.current;
setValue('skills', restored, { shouldDirty: true });
- setValue('skills_enabled', restored.length > 0, { shouldDirty: true });
+ const transition = skillsSelectionTransition(
+ restored,
+ getValues('skills_enabled'),
+ getValues('skill_authoring_enabled'),
+ getValues('skills_scope'),
+ );
+ if (transition.enabled !== undefined) {
+ setValue('skills_enabled', transition.enabled, { shouldDirty: true });
+ }
+ if (transition.authoringEnabled !== undefined) {
+ setValue('skill_authoring_enabled', transition.authoringEnabled, { shouldDirty: true });
+ }
+ if (transition.scope !== undefined) {
+ setValue('skills_scope', transition.scope, { shouldDirty: true });
+ }
},
[getValues, setValue],
);
@@ -168,9 +227,22 @@ export default function ToolsSection({ agentId }: Props) {
const current = (getValues('skills') ?? []) as string[];
const next = current.filter((s) => s !== patch.id);
setValue('skills', next, { shouldDirty: true });
- const flag = skillsEnabledTransition(next, getValues('skills_enabled'));
- if (flag !== undefined) {
- setValue('skills_enabled', flag, { shouldDirty: true });
+ const transition = skillsSelectionTransition(
+ next,
+ getValues('skills_enabled'),
+ getValues('skill_authoring_enabled'),
+ getValues('skills_scope'),
+ );
+ if (transition.enabled !== undefined) {
+ setValue('skills_enabled', transition.enabled, { shouldDirty: true });
+ }
+ if (transition.authoringEnabled !== undefined) {
+ setValue('skill_authoring_enabled', transition.authoringEnabled, {
+ shouldDirty: true,
+ });
+ }
+ if (transition.scope !== undefined) {
+ setValue('skills_scope', transition.scope, { shouldDirty: true });
}
break;
}
@@ -288,9 +360,35 @@ export default function ToolsSection({ agentId }: Props) {
onInfo={setDialogItem}
onRemove={handleQuickRemove}
badgeText={useAllSkills ? localize('com_ui_all_proper') : undefined}
- showAdd={!useAllSkills}
- showBody={!useAllSkills}
+ showAdd={skillsCapabilityEnabled && !useAllSkills}
+ showBody={skillsCapabilityEnabled && !useAllSkills}
>
+
+
+
+ {localize('com_ui_skills_enable')}
+
+
+
+
+
+
+ {localize('com_ui_skills_enable_hint')}
+
+
+
+
+
+
+
diff --git a/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx b/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx
index 38672bb5e4..75f8bff4db 100644
--- a/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
+import { SkillsScope } from 'librechat-data-provider';
import { render, screen, fireEvent } from '@testing-library/react';
import type { AgentItem } from '../items/types';
import ToolsSection from '../ToolsSection';
@@ -89,10 +90,14 @@ jest.mock('@librechat/client', () => ({
Switch: ({
id,
checked,
+ disabled,
+ 'aria-labelledby': ariaLabelledBy,
onCheckedChange,
}: {
id?: string;
checked?: boolean;
+ disabled?: boolean;
+ 'aria-labelledby'?: string;
onCheckedChange?: (value: boolean) => void;
}) => (