🎚️ feat: Explicit "Use All Skills" Toggle in the Skills Section (#14125)

* 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.
This commit is contained in:
Marco Beretta 2026-07-06 15:25:55 +02:00 • committed by GitHub
parent 8fcb77fe6f
commit a0aa1f2b9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 254 additions and 113 deletions

View file

@ -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 (
<div className="mb-1 flex w-full flex-col gap-4 text-sm">
<header className="grid grid-cols-[auto_1fr_auto] items-center gap-2 pt-1">
@ -64,7 +52,6 @@ export default function AdvancedPanel() {
<section className="flex flex-col gap-3">
<span className={groupHeadingClass}>{localize('com_ui_essentials')}</span>
<MaxAgentSteps />
{showSkillsKillSwitch && <SkillsToggle />}
</section>
<OrchestrationHub currentAgentId={currentAgentId} />

View file

@ -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<AgentForm>();
return (
<Controller
name="skills_enabled"
control={control}
render={({ field }) => (
<ToggleSetting
id={id}
label={localize('com_ui_tools_skills_enabled_kill_switch')}
checked={field.value === true}
onCheckedChange={(value) => field.onChange(Boolean(value))}
info={
<p className="text-sm text-text-secondary">
{localize('com_ui_tools_skills_enabled_kill_switch_hint')}
</p>
}
/>
)}
/>
);
}

View file

@ -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],

View file

@ -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 });
}

View file

@ -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<string | null>(null);
const [pendingMcpRemoval, setPendingMcpRemoval] = useState<string | null>(null);
const { getValues, setValue } = useFormContext<AgentForm>();
const { control, getValues, setValue } = useFormContext<AgentForm>();
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<string[]>([]);
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}
>
<div className="mb-1.5 flex items-center justify-between gap-3 px-1">
<div className="flex min-w-0 items-center gap-1.5">
<span
id="use-all-skills-label"
className="truncate text-[13px] font-medium text-text-primary"
>
{localize('com_ui_skills_use_all')}
</span>
<HoverCard openDelay={50}>
<InfoTrigger />
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<p className="text-sm text-text-secondary">
{localize('com_ui_skills_use_all_hint')}
</p>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
</div>
<Switch
id="use-all-skills"
checked={useAllSkills}
onCheckedChange={handleUseAllSkillsChange}
aria-labelledby="use-all-skills-label"
/>
</div>
</SelectedSection>
)}
{open && <ToolsMarketplaceDialog open={open} onOpenChange={setOpen} agentId={agentId} />}
{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 (
<div className="mb-3 flex flex-col">
<div className="mb-1 flex items-center justify-between">
<label className="block text-[11px] font-medium uppercase tracking-wide text-text-secondary">
{title}
{items.length > 0 && (
{badge != null && (
<span className="ml-1.5 inline-flex h-4 min-w-[16px] items-center justify-center rounded-full bg-surface-tertiary px-1.5 text-[10px] font-medium normal-case tracking-normal text-text-secondary">
{items.length}
{badge}
</span>
)}
</label>
@ -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',
)}
>
<Plus className="h-3.5 w-3.5" strokeWidth={1.75} aria-hidden="true" />
{localize('com_ui_add')}
</button>
</div>
{items.length === 0 ? (
<button
type="button"
onClick={onAdd}
className="flex w-full flex-col items-center gap-1 rounded-xl border border-dashed border-border-light px-2 py-4 text-text-secondary transition-colors hover:border-border-medium hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<Plus className="h-4 w-4" aria-hidden="true" />
<span className="text-xs">{emptyLabel}</span>
<span className="text-[11px] text-text-secondary">{emptyHint}</span>
</button>
) : (
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={`${item.kind}:${item.id}`}>
<ToolRow item={item} onInfo={onInfo} onRemove={onRemove} />
</li>
))}
</ul>
)}
{children}
<Collapse open={showBody}>
{items.length === 0 ? (
<button
type="button"
onClick={onAdd}
className="flex w-full flex-col items-center gap-1 rounded-xl border border-dashed border-border-light px-2 py-4 text-text-secondary transition-colors hover:border-border-medium hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<Plus className="h-4 w-4" aria-hidden="true" />
<span className="text-xs">{emptyLabel}</span>
<span className="text-[11px] text-text-secondary">{emptyHint}</span>
</button>
) : (
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={`${item.kind}:${item.id}`}>
<ToolRow item={item} onInfo={onInfo} onRemove={onRemove} />
</li>
))}
</ul>
)}
</Collapse>
</div>
);
}

View file

@ -11,14 +11,15 @@ let mockFileEntries: {
codeFiles: unknown[];
} = { contextFiles: [], knowledgeFiles: [], codeFiles: [] };
const mockSetValue = jest.fn();
let mockFormValues: Record<string, unknown> = {};
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 ? <div>{children}</div> : null,
OGDialogTemplate: () => null,
Switch: ({
id,
checked,
onCheckedChange,
}: {
id?: string;
checked?: boolean;
onCheckedChange?: (value: boolean) => void;
}) => (
<button
type="button"
id={id}
role="switch"
aria-checked={checked}
onClick={() => onCheckedChange?.(!checked)}
/>
),
HoverCard: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
HoverCardPortal: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
HoverCardContent: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
HoverCardTrigger: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
CircleHelpIcon: () => <svg aria-hidden="true" />,
}));
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(<ToolsSection agentId="a" />);
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(<ToolsSection agentId="a" />);
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(<ToolsSection agentId="a" />);
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(<ToolsSection agentId="a" />);
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(<ToolsSection agentId="a" />);
fireEvent.click(screen.getByRole('switch'));
mockFormValues = { skills: [], skills_enabled: true };
rerender(<ToolsSection agentId="a" />);
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(<ToolsSection agentId="a" />);
fireEvent.click(screen.getByRole('switch'));
mockFormValues = { skills: [], skills_enabled: true };
rerender(<ToolsSection agentId="b" />);
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(<ToolsSection agentId="a" />);
fireEvent.click(screen.getByRole('switch'));
expect(mockSetValue).toHaveBeenCalledWith('skills', [], { shouldDirty: true });
expect(mockSetValue).toHaveBeenCalledWith('skills_enabled', false, { shouldDirty: true });
});
});

View file

@ -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();
});
});

View file

@ -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;
}

View file

@ -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",