diff --git a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
index ba3abe1afa..79249496fb 100644
--- a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
+++ b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
@@ -1,15 +1,13 @@
-import { useMemo, useState } from 'react';
+import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useToastContext } from '@librechat/client';
import { ChevronLeft, Check, Copy } from 'lucide-react';
-import { AgentCapabilities, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import { sectionLabelClass, groupHeadingClass } from './ui';
-import { useLocalize, useHasAccess } from '~/hooks';
import { useAgentPanelContext } from '~/Providers';
import OrchestrationHub from './OrchestrationHub';
import MaxAgentSteps from './MaxAgentSteps';
-import SkillsToggle from './SkillsToggle';
+import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function AdvancedPanel() {
@@ -19,7 +17,7 @@ export default function AdvancedPanel() {
const currentAgentId = watch('id');
const [copied, setCopied] = useState(false);
- const { agentsConfig, setActivePanel } = useAgentPanelContext();
+ const { setActivePanel } = useAgentPanelContext();
const handleCopyAgentId = async () => {
if (!currentAgentId) return;
@@ -33,16 +31,6 @@ export default function AdvancedPanel() {
}
};
- const skillsEnabled = useMemo(
- () => agentsConfig?.capabilities.includes(AgentCapabilities.skills) ?? false,
- [agentsConfig],
- );
- const hasSkillsAccess = useHasAccess({
- permissionType: PermissionTypes.SKILLS,
- permission: Permissions.USE,
- });
- const showSkillsKillSwitch = skillsEnabled && hasSkillsAccess;
-
return (
@@ -64,7 +52,6 @@ export default function AdvancedPanel() {
{localize('com_ui_essentials')}
- {showSkillsKillSwitch && }
diff --git a/client/src/components/SidePanel/Agents/Advanced/SkillsToggle.tsx b/client/src/components/SidePanel/Agents/Advanced/SkillsToggle.tsx
deleted file mode 100644
index 0b8040b57e..0000000000
--- a/client/src/components/SidePanel/Agents/Advanced/SkillsToggle.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useFormContext, Controller } from 'react-hook-form';
-import type { AgentForm } from '~/common';
-import { useLocalize } from '~/hooks';
-import { ToggleSetting } from './ui';
-
-const id = 'skills_enabled_killswitch';
-
-export default function SkillsToggle() {
- const localize = useLocalize();
- const { control } = useFormContext();
-
- return (
- (
- field.onChange(Boolean(value))}
- info={
-
- {localize('com_ui_tools_skills_enabled_kill_switch_hint')}
-
- }
- />
- )}
- />
- );
-}
diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx
index 3e18fb4d7c..0deebd96bb 100644
--- a/client/src/components/SidePanel/Agents/AgentSelect.tsx
+++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx
@@ -150,6 +150,20 @@ function AgentSelect({
}
});
+ /** Legacy state from the removed Advanced kill switch: a non-empty
+ * allowlist with the master flag off (or unset, for agents predating
+ * the flag). The builder has no control left for it and the runtime
+ * treats it as "no skills", yet the section would render the selection
+ * as active. Normalize to enabled so the form matches what the UI
+ * shows and a later save persists the displayed behavior. */
+ if (
+ Array.isArray(formValues.skills) &&
+ formValues.skills.length > 0 &&
+ formValues.skills_enabled !== true
+ ) {
+ formValues.skills_enabled = true;
+ }
+
reset(formValues);
},
[reset],
diff --git a/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx b/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
index ab9610c852..08b934f580 100644
--- a/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx
@@ -111,9 +111,8 @@ export default function SkillsDialog({ open, onOpenChange, agentId }: SkillsDial
const applySkillsSelection = useCallback(
(next: string[]) => {
- const current = (getValues('skills') ?? []) as string[];
setValue('skills', next, { shouldDirty: true });
- const flag = skillsEnabledTransition(current, next, getValues('skills_enabled'));
+ const flag = skillsEnabledTransition(next, getValues('skills_enabled'));
if (flag !== undefined) {
setValue('skills_enabled', flag, { shouldDirty: true });
}
diff --git a/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx b/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
index 901953f855..23cfdfbc32 100644
--- a/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx
@@ -1,9 +1,19 @@
-import { useState, useMemo, useCallback } from 'react';
+import { useState, useMemo, useCallback, useRef } from 'react';
import { Plus } from 'lucide-react';
-import { useFormContext } from 'react-hook-form';
-import { Label, OGDialog, OGDialogTemplate, useToastContext } from '@librechat/client';
+import { useFormContext, useWatch } from 'react-hook-form';
import { PermissionTypes, Permissions, AgentCapabilities } from 'librechat-data-provider';
+import {
+ Label,
+ Switch,
+ OGDialog,
+ HoverCard,
+ HoverCardPortal,
+ HoverCardContent,
+ OGDialogTemplate,
+ useToastContext,
+} from '@librechat/client';
import type { TPlugin } from 'librechat-data-provider';
+import type { ReactNode } from 'react';
import type { AgentItem } from './items/types';
import type { AgentForm } from '~/common';
import {
@@ -18,10 +28,13 @@ import { useRemoveMCPTool, useVisibleTools } from '~/hooks/MCP';
import ToolsMarketplaceDialog from './ToolsMarketplaceDialog';
import { useLocalize, useHasAccess } from '~/hooks';
import { useAgentPanelContext } from '~/Providers';
+import { isEphemeralAgent, ESide } from '~/common';
import ItemDialog from './ItemDialog/ItemDialog';
-import { isEphemeralAgent } from '~/common';
+import { InfoTrigger } from '../Advanced/ui';
+import { Collapse } from '~/components/ui';
import SkillsDialog from './SkillsDialog';
import ToolRow from './ToolRow';
+import { cn } from '~/utils';
interface Props {
agentId: string;
@@ -36,7 +49,7 @@ export default function ToolsSection({ agentId }: Props) {
const [pendingActionRemoval, setPendingActionRemoval] = useState(null);
const [pendingMcpRemoval, setPendingMcpRemoval] = useState(null);
- const { getValues, setValue } = useFormContext();
+ const { control, getValues, setValue } = useFormContext();
const { agentsConfig, regularTools, mcpServersMap } = useAgentPanelContext();
const { removeTool: removeMCPTool } = useRemoveMCPTool();
const deleteAgentAction = useDeleteAgentAction({
@@ -66,6 +79,36 @@ export default function ToolsSection({ agentId }: Props) {
const { data: skillsData } = useListSkillsQuery({ limit: 100 }, { enabled: showSkills });
const resolvedSkills = useResolvedSkills(skillsData?.skills);
+ const skillsValue = useWatch({ control, name: 'skills' });
+ const skillsEnabledValue = useWatch({ control, name: 'skills_enabled' });
+ const useAllSkills = skillsEnabledValue === true && (skillsValue ?? []).length === 0;
+ /** 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
+ * resets), so a stale stash could otherwise restore one agent's allowlist
+ * into another. */
+ const stashedSkillsRef = useRef([]);
+ const [prevAgentId, setPrevAgentId] = useState(agentId);
+ if (prevAgentId !== agentId) {
+ setPrevAgentId(agentId);
+ stashedSkillsRef.current = [];
+ }
+
+ const handleUseAllSkillsChange = useCallback(
+ (checked: boolean) => {
+ if (checked) {
+ stashedSkillsRef.current = (getValues('skills') ?? []) as string[];
+ setValue('skills', [], { shouldDirty: true });
+ setValue('skills_enabled', true, { shouldDirty: true });
+ return;
+ }
+ const restored = stashedSkillsRef.current;
+ setValue('skills', restored, { shouldDirty: true });
+ setValue('skills_enabled', restored.length > 0, { shouldDirty: true });
+ },
+ [getValues, setValue],
+ );
+
const uninstallToolCredentials = useUninstallToolCredentials();
const { knowledgeFiles, codeFiles } = useAgentFileEntries();
@@ -123,7 +166,7 @@ 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(current, next, getValues('skills_enabled'));
+ const flag = skillsEnabledTransition(next, getValues('skills_enabled'));
if (flag !== undefined) {
setValue('skills_enabled', flag, { shouldDirty: true });
}
@@ -240,7 +283,37 @@ export default function ToolsSection({ agentId }: Props) {
onAdd={() => setSkillsOpen(true)}
onInfo={setDialogItem}
onRemove={handleQuickRemove}
- />
+ badgeText={useAllSkills ? localize('com_ui_all_proper') : undefined}
+ showAdd={!useAllSkills}
+ showBody={!useAllSkills}
+ >
+
+
+
+ {localize('com_ui_skills_use_all')}
+
+
+
+
+
+
+ {localize('com_ui_skills_use_all_hint')}
+
+
+
+
+
+
+
+
)}
{open && }
{skillsOpen && (
@@ -310,6 +383,12 @@ interface SelectedSectionProps {
onAdd: () => void;
onInfo: (item: AgentItem) => void;
onRemove: (item: AgentItem) => void;
+ /** Replaces the item-count badge next to the title. */
+ badgeText?: string;
+ showAdd?: boolean;
+ showBody?: boolean;
+ /** Rendered between the header and the item list / empty state. */
+ children?: ReactNode;
}
function SelectedSection({
@@ -321,16 +400,21 @@ function SelectedSection({
onAdd,
onInfo,
onRemove,
+ badgeText,
+ showAdd = true,
+ showBody = true,
+ children,
}: SelectedSectionProps) {
const localize = useLocalize();
+ const badge = badgeText ?? (items.length > 0 ? String(items.length) : undefined);
return (
@@ -338,31 +422,39 @@ function SelectedSection({
type="button"
onClick={onAdd}
aria-label={addLabel}
- className="inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-xs font-medium text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
+ aria-hidden={!showAdd || undefined}
+ tabIndex={showAdd ? 0 : -1}
+ className={cn(
+ 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-xs font-medium text-text-secondary transition hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
+ !showAdd && 'pointer-events-none opacity-0',
+ )}
>
{localize('com_ui_add')}
- {items.length === 0 ? (
-
- ) : (
-
- {items.map((item) => (
- -
-
-
- ))}
-
- )}
+ {children}
+
+ {items.length === 0 ? (
+
+ ) : (
+
+ {items.map((item) => (
+ -
+
+
+ ))}
+
+ )}
+
);
}
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 87f4d3242e..6e68baee12 100644
--- a/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx
+++ b/client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx
@@ -11,14 +11,15 @@ let mockFileEntries: {
codeFiles: unknown[];
} = { contextFiles: [], knowledgeFiles: [], codeFiles: [] };
const mockSetValue = jest.fn();
+let mockFormValues: Record = {};
jest.mock('react-hook-form', () => ({
useFormContext: () => ({
control: {},
- getValues: () => [],
+ getValues: (name: string) => mockFormValues[name],
setValue: mockSetValue,
}),
- useWatch: () => undefined,
+ useWatch: ({ name }: { name: string }) => mockFormValues[name],
}));
jest.mock('~/hooks', () => ({
@@ -73,6 +74,28 @@ jest.mock('@librechat/client', () => ({
OGDialog: ({ open, children }: { open?: boolean; children?: React.ReactNode }) =>
open ? {children}
: null,
OGDialogTemplate: () => null,
+ Switch: ({
+ id,
+ checked,
+ onCheckedChange,
+ }: {
+ id?: string;
+ checked?: boolean;
+ onCheckedChange?: (value: boolean) => void;
+ }) => (
+