From a0aa1f2b9d3f45e20bc13133668962477f632ba0 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Mon, 6 Jul 2026 15:25:55 +0200
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=9A=EF=B8=8F=20feat:=20Explicit=20"Use?=
=?UTF-8?q?=20All=20Skills"=20Toggle=20in=20the=20Skills=20Section=20(#141?=
=?UTF-8?q?25)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: Add explicit Use All Skills toggle to the Skills section
Adding a skill to an agent could silently mean "all skills enabled"
(skills_enabled on with an empty allowlist), and the only master control
was the Agent Skills kill switch buried in the Advanced panel, which
implied skills required a trip to Advanced at all.
- Add a "Use all skills" switch directly in the builder's Skills
section: on = the agent gets every skill the user can access,
including skills added in the future (skills_enabled true, empty
allowlist). While on, the per-skill list and Add button are hidden
and the header shows an All badge; turning it off restores the
previously selected skills.
- Remove the kill switch from the Advanced panel.
- Simplify skillsEnabledTransition so the master flag tracks the
selection on every edit, which also re-enables agents left with a
selection but a disabled flag by the old kill switch.
* fix: Normalize legacy disabled allowlists and scope the use-all stash per agent
Agents saved with the removed Advanced kill switch off (or predating the
skills_enabled flag) load a non-empty allowlist with the master flag not
true. The builder rendered that selection as active while the runtime
ignored it, with no control left to re-enable. Normalize on form load so
the flag matches the displayed selection.
The use-all stash lives in a ref and ToolsSection is not remounted when
switching agents (only the form resets), so toggling off on another agent
could restore the previous agent's allowlist. Clear the stash when the
agent id changes.
---
.../Agents/Advanced/AdvancedPanel.tsx | 19 +--
.../Agents/Advanced/SkillsToggle.tsx | 31 ----
.../SidePanel/Agents/AgentSelect.tsx | 14 ++
.../SidePanel/Agents/Tools/SkillsDialog.tsx | 3 +-
.../SidePanel/Agents/Tools/ToolsSection.tsx | 150 ++++++++++++++----
.../Tools/__tests__/ToolsSection.spec.tsx | 92 ++++++++++-
.../Tools/items/__tests__/mutations.spec.ts | 29 ++--
.../SidePanel/Agents/Tools/items/mutations.ts | 25 ++-
client/src/locales/en/translation.json | 4 +-
9 files changed, 254 insertions(+), 113 deletions(-)
delete mode 100644 client/src/components/SidePanel/Agents/Advanced/SkillsToggle.tsx
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 (
{title}
- {items.length > 0 && (
+ {badge != null && (
- {items.length}
+ {badge}
)}
@@ -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 ? (
-
-
- {emptyLabel}
- {emptyHint}
-
- ) : (
-
- {items.map((item) => (
-
-
-
- ))}
-
- )}
+ {children}
+
+ {items.length === 0 ? (
+
+
+ {emptyLabel}
+ {emptyHint}
+
+ ) : (
+
+ {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;
+ }) => (
+ onCheckedChange?.(!checked)}
+ />
+ ),
+ HoverCard: ({ children }: { children?: React.ReactNode }) => {children}
,
+ HoverCardPortal: ({ children }: { children?: React.ReactNode }) => {children}
,
+ HoverCardContent: ({ children }: { children?: React.ReactNode }) => {children}
,
+ HoverCardTrigger: ({ children }: { children?: React.ReactNode }) => {children}
,
+ CircleHelpIcon: () => ,
}));
jest.mock('../ToolsMarketplaceDialog', () => ({
@@ -91,6 +114,7 @@ const fileSearchItem: AgentItem = {
beforeEach(() => {
mockSelected = [];
mockFileEntries = { contextFiles: [], knowledgeFiles: [], codeFiles: [] };
+ mockFormValues = {};
mockSetValue.mockClear();
});
@@ -137,3 +161,67 @@ describe('ToolsSection', () => {
expect(mockSetValue).toHaveBeenCalledWith('file_search', false, { shouldDirty: true });
});
});
+
+describe('use all skills toggle', () => {
+ test('renders off inside the Skills section by default', () => {
+ render( );
+ expect(screen.getByText('com_ui_skills_use_all')).toBeInTheDocument();
+ expect(screen.getByText('com_ui_skills_use_all_hint')).toBeInTheDocument();
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
+ });
+
+ test('turning it on clears the selection and enables the master flag', () => {
+ mockFormValues = { skills: ['s1'], skills_enabled: true };
+ render( );
+ fireEvent.click(screen.getByRole('switch'));
+ expect(mockSetValue).toHaveBeenCalledWith('skills', [], { shouldDirty: true });
+ expect(mockSetValue).toHaveBeenCalledWith('skills_enabled', true, { shouldDirty: true });
+ });
+
+ test('while on, hides Add and the skill list and shows the All badge', () => {
+ mockFormValues = { skills: [], skills_enabled: true };
+ render( );
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
+ expect(screen.queryByRole('button', { name: 'com_ui_add_skills' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /com_ui_skills_empty/ })).not.toBeInTheDocument();
+ expect(screen.getByText('com_ui_all_proper')).toBeInTheDocument();
+ });
+
+ test('clicking the label does not toggle the switch', () => {
+ render( );
+ fireEvent.click(screen.getByText('com_ui_skills_use_all'));
+ expect(mockSetValue).not.toHaveBeenCalled();
+ });
+
+ test('turning it off restores the previously selected skills', () => {
+ mockFormValues = { skills: ['s1'], skills_enabled: true };
+ const { rerender } = render( );
+ fireEvent.click(screen.getByRole('switch'));
+ mockFormValues = { skills: [], skills_enabled: true };
+ rerender( );
+ mockSetValue.mockClear();
+ fireEvent.click(screen.getByRole('switch'));
+ expect(mockSetValue).toHaveBeenCalledWith('skills', ['s1'], { shouldDirty: true });
+ expect(mockSetValue).toHaveBeenCalledWith('skills_enabled', true, { shouldDirty: true });
+ });
+
+ test('does not restore a stash from a different agent', () => {
+ mockFormValues = { skills: ['s1'], skills_enabled: true };
+ const { rerender } = render( );
+ fireEvent.click(screen.getByRole('switch'));
+ mockFormValues = { skills: [], skills_enabled: true };
+ rerender( );
+ mockSetValue.mockClear();
+ fireEvent.click(screen.getByRole('switch'));
+ expect(mockSetValue).toHaveBeenCalledWith('skills', [], { shouldDirty: true });
+ expect(mockSetValue).toHaveBeenCalledWith('skills_enabled', false, { shouldDirty: true });
+ });
+
+ test('turning it off with nothing stashed disables the master flag', () => {
+ mockFormValues = { skills: [], skills_enabled: true };
+ render( );
+ fireEvent.click(screen.getByRole('switch'));
+ expect(mockSetValue).toHaveBeenCalledWith('skills', [], { shouldDirty: true });
+ expect(mockSetValue).toHaveBeenCalledWith('skills_enabled', false, { shouldDirty: true });
+ });
+});
diff --git a/client/src/components/SidePanel/Agents/Tools/items/__tests__/mutations.spec.ts b/client/src/components/SidePanel/Agents/Tools/items/__tests__/mutations.spec.ts
index 7db3648af1..e485de9d16 100644
--- a/client/src/components/SidePanel/Agents/Tools/items/__tests__/mutations.spec.ts
+++ b/client/src/components/SidePanel/Agents/Tools/items/__tests__/mutations.spec.ts
@@ -132,28 +132,23 @@ describe('computeToggleAction', () => {
});
describe('skillsEnabledTransition', () => {
- test('adding the first skill turns the master flag on', () => {
- expect(skillsEnabledTransition([], ['s1'], undefined)).toBe(true);
- expect(skillsEnabledTransition([], ['s1'], false)).toBe(true);
+ test('a non-empty selection turns the master flag on when it is off', () => {
+ expect(skillsEnabledTransition(['s1'], undefined)).toBe(true);
+ expect(skillsEnabledTransition(['s1'], false)).toBe(true);
+ expect(skillsEnabledTransition(['s1', 's2'], false)).toBe(true);
});
- test('adding the first skill leaves an already-on flag alone', () => {
- expect(skillsEnabledTransition([], ['s1'], true)).toBeUndefined();
+ test('a non-empty selection leaves an already-on flag alone', () => {
+ expect(skillsEnabledTransition(['s1'], true)).toBeUndefined();
+ expect(skillsEnabledTransition(['s1', 's2'], true)).toBeUndefined();
});
- test('removing the last skill turns the master flag off', () => {
- expect(skillsEnabledTransition(['s1'], [], true)).toBe(false);
+ test('clearing the selection turns the master flag off', () => {
+ expect(skillsEnabledTransition([], true)).toBe(false);
});
- test('removing the last skill leaves an off flag alone', () => {
- expect(skillsEnabledTransition(['s1'], [], false)).toBeUndefined();
- expect(skillsEnabledTransition(['s1'], [], undefined)).toBeUndefined();
- });
-
- test('edits within a non-empty selection never touch the flag', () => {
- expect(skillsEnabledTransition(['s1'], ['s1', 's2'], true)).toBeUndefined();
- expect(skillsEnabledTransition(['s1'], ['s1', 's2'], false)).toBeUndefined();
- expect(skillsEnabledTransition(['s1', 's2'], ['s1'], true)).toBeUndefined();
- expect(skillsEnabledTransition(['s1', 's2'], ['s1'], false)).toBeUndefined();
+ test('an empty selection leaves an off flag alone', () => {
+ expect(skillsEnabledTransition([], false)).toBeUndefined();
+ expect(skillsEnabledTransition([], undefined)).toBeUndefined();
});
});
diff --git a/client/src/components/SidePanel/Agents/Tools/items/mutations.ts b/client/src/components/SidePanel/Agents/Tools/items/mutations.ts
index b65d844978..d4fed0a0c1 100644
--- a/client/src/components/SidePanel/Agents/Tools/items/mutations.ts
+++ b/client/src/components/SidePanel/Agents/Tools/items/mutations.ts
@@ -48,24 +48,21 @@ export function computeToggleAction(item: AgentItem, state: { selected: boolean
/**
* `skills_enabled` is the master opt-in for the skill allowlist, and an empty
- * allowlist with the flag on means the FULL accessible catalog. Selection
- * edits therefore sync the flag on empty/non-empty transitions: picking the
- * first skill turns it on so the choice takes effect, and removing the last
- * one turns it off so the agent doesn't silently escalate to every skill.
- * Edits within a non-empty selection return `undefined` (leave the flag
- * alone), preserving the Advanced kill switch's disable-without-clearing
- * behavior.
+ * allowlist with the flag on means the FULL accessible catalog ("use all
+ * skills"). Selection edits sync the flag to the selection: any non-empty
+ * selection needs the flag on to take effect (this also heals agents saved
+ * with the since-removed Advanced kill switch off while skills were still
+ * selected), and clearing the selection turns it off so the agent doesn't
+ * silently escalate to the full catalog. Returns `undefined` when the flag
+ * already matches. The "use all skills" state (flag on, empty selection) is
+ * only ever set by its explicit toggle, never by selection edits.
*/
export function skillsEnabledTransition(
- current: string[],
next: string[],
enabled: boolean | undefined,
): boolean | undefined {
- if (current.length === 0 && next.length > 0 && enabled !== true) {
- return true;
+ if (next.length > 0) {
+ return enabled === true ? undefined : true;
}
- if (current.length > 0 && next.length === 0 && enabled === true) {
- return false;
- }
- return undefined;
+ return enabled === true ? false : undefined;
}
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 6f68d343d9..ce904766ad 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -1794,6 +1794,8 @@
"com_ui_skills_load_error": "Failed to load skills",
"com_ui_skills_manual_invoked": "Manually invoked skills",
"com_ui_skills_queued": "Skills queued for next submission",
+ "com_ui_skills_use_all": "Use all skills",
+ "com_ui_skills_use_all_hint": "The agent can use every skill available to you, including skills added in the future.",
"com_ui_sort_by": "Sort by",
"com_ui_sort_chats_by": "Sort chats by",
"com_ui_sort_created": "Created",
@@ -1903,8 +1905,6 @@
"com_ui_tools_search_no_results": "Nothing matched your search",
"com_ui_tools_section_title": "Tools",
"com_ui_tools_shared_by": "Shared by {{name}}",
- "com_ui_tools_skills_enabled_kill_switch": "Agent skills",
- "com_ui_tools_skills_enabled_kill_switch_hint": "Master switch for this agent's skills. When off, the agent ignores every skill you've selected, letting you disable them all at once without clearing your selection.",
"com_ui_tools_view_favorites": "Favorites",
"com_ui_tools_view_favorites_empty": "You haven't favorited anything yet",
"com_ui_tools_view_made_by_you": "Made by you",