From 21766b5b3cbfa96ed049eaebdcd429b9fb39919f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 21 Jul 2026 12:37:51 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A0=20fix:=20Restore=20Agent=20Memory?= =?UTF-8?q?=20Scope=20Control=20in=20Unified=20Builder=20(#14292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧠 fix: Restore Agent Memory Scope Control in Unified Builder The Agent Builder redesign (#13952) surfaced memory as a Tools marketplace item and stopped rendering SidePanel/Agents/Memory.tsx, orphaning the file and removing the 'Keep memories separate for this agent' control shipped in #14084. Only the render was lost: the locale keys, memory_scope on AgentForm, the AgentPanel save path, and AgentSelect hydration all survived, which is why setting memory_scope directly on the agent document still worked. Restore it as a builtin item setting (the seam the new builder uses for per-tool config): mark memory configurable so its row gets a cog, and render a MemoryConfig branch in BuiltinSection mirroring ArtifactsConfig. Delete the orphaned component so there is a single source of truth. Resolves #14287 * 🧹 chore: Remove Orphaned com_agents_enable_memory i18n Key The key labeled the enable checkbox in the deleted SidePanel/Agents/Memory.tsx. The unified builder labels the catalog item via com_ui_memory, so it has no remaining consumer and detect-unused-i18n-keys fails on it. --- .../components/SidePanel/Agents/Memory.tsx | 118 ------------------ .../__tests__/BuiltinSection.spec.tsx | 80 ++++++++++++ .../ItemDialog/sections/BuiltinSection.tsx | 49 +++++++- .../items/__tests__/configurable.spec.ts | 6 +- .../Agents/Tools/items/configurable.ts | 1 + client/src/locales/en/translation.json | 1 - 6 files changed, 131 insertions(+), 124 deletions(-) delete mode 100644 client/src/components/SidePanel/Agents/Memory.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/BuiltinSection.spec.tsx diff --git a/client/src/components/SidePanel/Agents/Memory.tsx b/client/src/components/SidePanel/Agents/Memory.tsx deleted file mode 100644 index 69c409f8f6..0000000000 --- a/client/src/components/SidePanel/Agents/Memory.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { memo } from 'react'; -import { useFormContext, Controller, useWatch } from 'react-hook-form'; -import { MemoryScope, AgentCapabilities } from 'librechat-data-provider'; -import { - Checkbox, - HoverCard, - HoverCardContent, - HoverCardPortal, - HoverCardTrigger, - CircleHelpIcon, -} from '@librechat/client'; -import type { AgentForm } from '~/common'; -import { useLocalize } from '~/hooks'; -import { ESide } from '~/common'; - -function Memory() { - const localize = useLocalize(); - const methods = useFormContext(); - const { control } = methods; - const memoryEnabled = useWatch({ control, name: AgentCapabilities.memory }); - - return ( -
- -
- ( - - )} - /> - - - - - - -
-

{localize('com_agents_memory_info')}

-
-
-
-
-
- {memoryEnabled === true && ( - -
- ( - - field.onChange(checked === true ? MemoryScope.agent : MemoryScope.user) - } - className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" - value={(field.value === MemoryScope.agent).toString()} - aria-labelledby="memory-scope-label" - /> - )} - /> - - - - - - -
-

- {localize('com_agents_memory_scope_info')} -

-
-
-
-
-
- )} -
- ); -} - -export default memo(Memory); diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/BuiltinSection.spec.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/BuiltinSection.spec.tsx new file mode 100644 index 0000000000..938240e40b --- /dev/null +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/BuiltinSection.spec.tsx @@ -0,0 +1,80 @@ +import '@testing-library/jest-dom/extend-expect'; +import { MemoryScope } from 'librechat-data-provider'; +import { useForm, FormProvider, useWatch } from 'react-hook-form'; +import { render, screen, fireEvent } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import type { AgentForm } from '~/common'; +import BuiltinSection from '../sections/BuiltinSection'; + +jest.mock('~/hooks', () => ({ useLocalize: () => (key: string) => key })); +jest.mock('~/data-provider', () => ({ useVerifyAgentToolAuth: () => ({ data: undefined }) })); +jest.mock('../../../Search/Action', () => ({ __esModule: true, default: () =>
})); +jest.mock('../../../FileContext', () => ({ __esModule: true, default: () =>
})); +jest.mock('../../../FileSearch', () => ({ __esModule: true, default: () =>
})); +jest.mock('../../../Code/Files', () => ({ __esModule: true, default: () =>
})); + +function ScopeProbe() { + const value = useWatch({ name: 'memory_scope' }); + return {String(value)}; +} + +function renderSection(builtinId: string, defaultValues: Partial = {}) { + function Wrapper({ children }: { children: ReactNode }) { + const methods = useForm({ defaultValues: defaultValues as AgentForm }); + return ( + + {children} + + + ); + } + + return render( + , + { wrapper: Wrapper }, + ); +} + +const scopeCheckbox = () => screen.getByRole('checkbox', { name: 'com_agents_memory_scope' }); + +describe('BuiltinSection memory scope', () => { + test('renders the scope control unchecked when memory is on the shared pool', () => { + renderSection('memory', { memory: true, memory_scope: MemoryScope.user }); + expect(scopeCheckbox()).not.toBeChecked(); + expect(screen.getByText('com_agents_memory_scope_info')).toBeInTheDocument(); + }); + + test('defaults to the shared pool when the agent has no saved scope', () => { + renderSection('memory', { memory: true }); + expect(scopeCheckbox()).not.toBeChecked(); + }); + + test('renders checked for an agent already partitioned to its own memories', () => { + renderSection('memory', { memory: true, memory_scope: MemoryScope.agent }); + expect(scopeCheckbox()).toBeChecked(); + }); + + test('toggling writes the scope back to the form in both directions', () => { + renderSection('memory', { memory: true, memory_scope: MemoryScope.user }); + + fireEvent.click(scopeCheckbox()); + expect(screen.getByTestId('scope')).toHaveTextContent(MemoryScope.agent); + expect(scopeCheckbox()).toBeChecked(); + + fireEvent.click(scopeCheckbox()); + expect(screen.getByTestId('scope')).toHaveTextContent(MemoryScope.user); + expect(scopeCheckbox()).not.toBeChecked(); + }); + + test('other builtins do not render the scope control', () => { + renderSection('execute_code', { memory: true, memory_scope: MemoryScope.agent }); + expect(screen.queryByRole('checkbox', { name: 'com_agents_memory_scope' })).toBeNull(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/BuiltinSection.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/BuiltinSection.tsx index f3b3e9dae3..adc6a14b22 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/BuiltinSection.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/BuiltinSection.tsx @@ -1,6 +1,6 @@ -import { Radio } from '@librechat/client'; +import { Radio, Checkbox } from '@librechat/client'; import { useFormContext, useWatch } from 'react-hook-form'; -import { Tools, ArtifactModes, AgentCapabilities } from 'librechat-data-provider'; +import { Tools, MemoryScope, ArtifactModes, AgentCapabilities } from 'librechat-data-provider'; import type { TranslationKeys } from '~/hooks/useLocalize'; import type { AgentForm, ExtendedFile } from '~/common'; import type { BuiltinId } from '../../items/types'; @@ -74,6 +74,43 @@ function ArtifactsConfig({ value, onChange }: ArtifactsConfigProps) { ); } +interface MemoryConfigProps { + value: string; + onChange: (next: MemoryScope) => void; +} + +function MemoryConfig({ value, onChange }: MemoryConfigProps) { + const localize = useLocalize(); + const isolated = value === MemoryScope.agent; + + return ( +
+
+ + onChange(checked === true ? MemoryScope.agent : MemoryScope.user) + } + className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" + value={isolated.toString()} + aria-labelledby="memory-scope-label" + /> + +
+

+ {localize('com_agents_memory_scope_info')} +

+
+ ); +} + function WebSearchConfig() { const { data } = useVerifyAgentToolAuth({ toolId: Tools.web_search }, { retry: 1 }); return ; @@ -91,6 +128,7 @@ export default function BuiltinSection({ const { control, setValue } = useFormContext(); const artifactsValue = (useWatch({ control, name: AgentCapabilities.artifacts }) ?? '') as string; + const memoryScope = (useWatch({ control, name: 'memory_scope' }) ?? MemoryScope.user) as string; let body: React.ReactNode = null; @@ -109,6 +147,13 @@ export default function BuiltinSection({ ); } else if (builtinId === 'context') { body = ; + } else if (builtinId === 'memory') { + body = ( + setValue('memory_scope', next, { shouldDirty: true })} + /> + ); } const localizedDescription = description ? localize(description as TranslationKeys) : ''; diff --git a/client/src/components/SidePanel/Agents/Tools/items/__tests__/configurable.spec.ts b/client/src/components/SidePanel/Agents/Tools/items/__tests__/configurable.spec.ts index aa12b7c3d9..741071890f 100644 --- a/client/src/components/SidePanel/Agents/Tools/items/__tests__/configurable.spec.ts +++ b/client/src/components/SidePanel/Agents/Tools/items/__tests__/configurable.spec.ts @@ -6,15 +6,15 @@ const builtin = (id: string, extra: Record = {}): AgentItem => ({ kind: 'builtin', id, name: '', description: '', iconKey: id, ...extra }) as AgentItem; describe('hasConfigurableSettings', () => { - test('artifacts, file_search, and context builtins are configurable', () => { + test('artifacts, file_search, context, and memory builtins are configurable', () => { expect(hasConfigurableSettings(builtin('artifacts'))).toBe(true); expect(hasConfigurableSettings(builtin('file_search'))).toBe(true); expect(hasConfigurableSettings(builtin('context'))).toBe(true); + expect(hasConfigurableSettings(builtin('memory'))).toBe(true); }); - test('execute_code and memory builtins are not configurable', () => { + test('execute_code builtin is not configurable', () => { expect(hasConfigurableSettings(builtin('execute_code'))).toBe(false); - expect(hasConfigurableSettings(builtin('memory'))).toBe(false); }); test('web_search is configurable only when auth is user-provided', () => { diff --git a/client/src/components/SidePanel/Agents/Tools/items/configurable.ts b/client/src/components/SidePanel/Agents/Tools/items/configurable.ts index 07bbdfecb6..8063c497d0 100644 --- a/client/src/components/SidePanel/Agents/Tools/items/configurable.ts +++ b/client/src/components/SidePanel/Agents/Tools/items/configurable.ts @@ -14,6 +14,7 @@ export function hasConfigurableSettings(item: AgentItem): boolean { item.id === 'artifacts' || item.id === 'file_search' || item.id === 'context' || + item.id === 'memory' || (item.id === 'web_search' && item.userProvidedAuth === true) ); case 'tool': diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 907147c5ce..cb6f0bd11e 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -41,7 +41,6 @@ "com_agents_description_card": "Description: {{description}}", "com_agents_description_placeholder": "What this agent does", "com_agents_empty_state_heading": "No agents found", - "com_agents_enable_memory": "Enable Memory", "com_agents_error_bad_request_message": "The request could not be processed.", "com_agents_error_bad_request_suggestion": "Please check your input and try again.", "com_agents_error_category_title": "Category Error",