🌉 chore: Gate Skills UI by Agent Capability Checks (#12793)

- Updated the Skills component to include a check for `skillsEnabled` from agent capabilities, ensuring skills are only displayed when both permissions and capabilities allow.
- Modified the `useHandleKeyUp` hook to prevent triggering skill commands when skills are disabled, enhancing user experience and preventing errors.
- Added tests to verify that skills commands do not trigger when skills are disabled, ensuring robust functionality.
- Refactored the `useSideNavLinks` hook to incorporate skills capability checks, ensuring consistent access control across the application.
This commit is contained in:
Danny Avila 2026-04-23 09:49:24 -07:00
parent d83cb84f59
commit 523994220d
4 changed files with 58 additions and 7 deletions

View file

@ -1,8 +1,8 @@
import React, { memo } from 'react';
import { ScrollText } from 'lucide-react';
import { CheckboxButton } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useLocalize, useHasAccess } from '~/hooks';
import { Permissions, PermissionTypes, defaultAgentCapabilities } from 'librechat-data-provider';
import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';
function Skills() {
@ -15,7 +15,11 @@ function Skills() {
permission: Permissions.USE,
});
if (!canUseSkills) {
const { skillsEnabled } = useAgentCapabilities(
context?.agentsConfig?.capabilities ?? defaultAgentCapabilities,
);
if (!canUseSkills || !skillsEnabled) {
return null;
}

View file

@ -5,6 +5,7 @@ const mockSetShowSkillsPopover = jest.fn();
const mockHasPromptsAccess = { current: true };
const mockHasMultiConvoAccess = { current: true };
const mockHasSkillsAccess = { current: true };
const mockSkillsEnabled = { current: true };
const mockEndpoint = { current: 'openAI' as string | null };
const mockCommandToggles = { at: true, plus: true, slash: true, dollar: true };
@ -76,6 +77,16 @@ jest.mock('~/hooks/Roles/useHasAccess', () =>
}),
);
jest.mock('~/hooks/Agents/useGetAgentsConfig', () =>
jest.fn(() => ({ agentsConfig: { capabilities: [] } })),
);
jest.mock('~/hooks/Agents/useAgentCapabilities', () =>
jest.fn(() => ({
skillsEnabled: mockSkillsEnabled.current,
})),
);
import React from 'react';
import { renderHook, act } from '@testing-library/react';
import useHandleKeyUp from './useHandleKeyUp';
@ -118,6 +129,7 @@ beforeEach(() => {
mockHasPromptsAccess.current = true;
mockHasMultiConvoAccess.current = true;
mockHasSkillsAccess.current = true;
mockSkillsEnabled.current = true;
mockEndpoint.current = 'openAI';
mockCommandToggles.at = true;
mockCommandToggles.plus = true;
@ -424,6 +436,16 @@ describe('useHandleKeyUp', () => {
expect(setShowSkillsPopover).not.toHaveBeenCalled();
});
it('does NOT trigger $ skill command when skills capability is disabled on agents endpoint', () => {
mockSkillsEnabled.current = false;
const ref = makeTextAreaRef('$', 1);
const { handleKeyUp, setShowSkillsPopover } = renderUseHandleKeyUp(ref);
act(() => handleKeyUp(makeKeyEvent('$')));
expect(setShowSkillsPopover).not.toHaveBeenCalled();
});
});
describe('endpoint gating', () => {

View file

@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { PermissionTypes, Permissions, isAssistantsEndpoint } from 'librechat-data-provider';
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
import useGetAgentsConfig from '~/hooks/Agents/useGetAgentsConfig';
import useHasAccess from '~/hooks/Roles/useHasAccess';
import store from '~/store';
@ -65,6 +67,8 @@ const useHandleKeyUp = ({
permissionType: PermissionTypes.SKILLS,
permission: Permissions.USE,
});
const { agentsConfig } = useGetAgentsConfig();
const { skillsEnabled } = useAgentCapabilities(agentsConfig?.capabilities);
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
const endpoint = useRecoilValue(store.effectiveEndpointByIndex(index));
const setShowMentionPopover = useSetRecoilState(store.showMentionPopoverFamily(index));
@ -109,13 +113,25 @@ const useHandleKeyUp = ({
}, [textAreaRef, hasPromptsAccess, setShowPromptsPopover, slashCommandEnabled]);
const handleSkillsCommand = useCallback(() => {
if (!hasSkillsAccess || !dollarCommandEnabled || isAssistantsEndpoint(endpoint)) {
if (
!hasSkillsAccess ||
!skillsEnabled ||
!dollarCommandEnabled ||
isAssistantsEndpoint(endpoint)
) {
return;
}
if (shouldTriggerCommand(textAreaRef, '$')) {
setShowSkillsPopover(true);
}
}, [textAreaRef, hasSkillsAccess, setShowSkillsPopover, dollarCommandEnabled, endpoint]);
}, [
textAreaRef,
hasSkillsAccess,
skillsEnabled,
setShowSkillsPopover,
dollarCommandEnabled,
endpoint,
]);
const commandHandlers = useMemo(
() => ({

View file

@ -19,6 +19,12 @@ import {
} from 'librechat-data-provider';
import type { TInterfaceConfig, TEndpointsConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import {
useAgentCapabilities,
useMCPServerManager,
useGetAgentsConfig,
useHasAccess,
} from '~/hooks';
import MCPBuilderPanel from '~/components/SidePanel/MCPBuilder/MCPBuilderPanel';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
@ -26,7 +32,6 @@ import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
import Parameters from '~/components/SidePanel/Parameters/Panel';
import { MemoryPanel } from '~/components/SidePanel/Memories';
import FilesPanel from '~/components/SidePanel/Files/Panel';
import { useHasAccess, useMCPServerManager } from '~/hooks';
import { PromptsAccordion } from '~/components/Prompts';
import { SkillsAccordion } from '~/components/Skills';
@ -85,6 +90,9 @@ export default function useSideNavLinks({
});
const { availableMCPServers } = useMCPServerManager();
const { agentsConfig } = useGetAgentsConfig({ endpointsConfig });
const { skillsEnabled } = useAgentCapabilities(agentsConfig?.capabilities);
const Links = useMemo(() => {
const links: NavLink[] = [];
@ -122,7 +130,7 @@ export default function useSideNavLinks({
});
}
if (hasAccessToSkills) {
if (hasAccessToSkills && skillsEnabled) {
links.push({
title: 'com_ui_skills',
label: '',
@ -217,6 +225,7 @@ export default function useSideNavLinks({
hasAccessToCreateAgents,
hasAccessToPrompts,
hasAccessToSkills,
skillsEnabled,
hasAccessToMemories,
hasAccessToReadMemories,
interfaceConfig.parameters,