🎽 fix: Commit Subagent Roster Selections to Form State (#15154)

* fix: persist subagent selections synchronously

* test: verify subagent roster form state

* style: sort subagent roster test imports
This commit is contained in:
Danny Avila 2026-08-24 03:21:48 -04:00 committed by GitHub
parent 092bc583a8
commit 8773b36eec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 120 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Switch } from '@librechat/client';
import { Network, Users } from 'lucide-react';
import type { ControllerRenderProps } from 'react-hook-form';
@ -16,7 +16,6 @@ interface AgentSubagentsProps {
const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId, maxSubagents }) => {
const localize = useLocalize();
const [newAgentId, setNewAgentId] = useState('');
const fieldValue = field.value;
const value = useMemo(() => fieldValue ?? {}, [fieldValue]);
@ -65,14 +64,19 @@ const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId,
[field, value],
);
useEffect(() => {
if (newAgentId && agentIds.length < maxSubagents && !agentIds.includes(newAgentId)) {
setAgentIds([...agentIds, newAgentId]);
setNewAgentId('');
} else if (newAgentId) {
setNewAgentId('');
}
}, [newAgentId, agentIds, maxSubagents, setAgentIds]);
const addAgent = useCallback(
(agentId: string) => {
if (!agentId || agentIds.length >= maxSubagents || agentIds.includes(agentId)) {
return;
}
/** Commit the selection directly to react-hook-form. Deferring this
* through component state and an effect allowed an immediate form submit
* to persist the enable toggles before the selected roster. */
setAgentIds([...agentIds, agentId]);
},
[agentIds, maxSubagents, setAgentIds],
);
const removeAgentAt = (index: number) => {
setAgentIds(agentIds.filter((_, i) => i !== index));
@ -140,7 +144,7 @@ const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId,
{agentIds.length < maxSubagents && (
<AddAgentSelect
options={options}
onSelect={setNewAgentId}
onSelect={addAgent}
placeholder={localize('com_ui_agent_subagents_add')}
ariaLabel={localize('com_ui_agent_subagents_add')}
/>

View file

@ -0,0 +1,105 @@
/**
* @jest-environment jsdom
*/
import { Controller, useForm } from 'react-hook-form';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { UseFormReturn } from 'react-hook-form';
import type { ReactNode } from 'react';
import type { AgentForm } from '~/common';
import AgentSubagents from '../AgentSubagents';
let mockSelectAgent: ((agentId: string) => void) | undefined;
let mockGetValues: UseFormReturn<AgentForm>['getValues'] | undefined;
const mockSubmit = jest.fn();
jest.mock('@librechat/client', () => ({
Switch: () => null,
}));
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string) => key,
}));
jest.mock('../AgentList', () => ({
AddAgentSelect: ({ onSelect }: { onSelect: (agentId: string) => void }) => {
mockSelectAgent = onSelect;
return null;
},
ListMeta: () => null,
StaticAgentRow: () => null,
useSelectableAgents: () => ({ options: [], getAgent: () => undefined }),
}));
jest.mock('../OrchestrationPattern', () => ({
__esModule: true,
default: ({ children, trailing }: { children: ReactNode; trailing: ReactNode }) => (
<>
{trailing}
{children}
</>
),
}));
jest.mock('../ui', () => ({
ToggleSetting: () => null,
}));
describe('AgentSubagents', () => {
beforeEach(() => {
mockSelectAgent = undefined;
mockGetValues = undefined;
mockSubmit.mockReset();
});
function Harness() {
const methods = useForm<AgentForm>({
defaultValues: {
subagents: {
enabled: true,
allowSelf: false,
agent_ids: [],
},
},
});
mockGetValues = methods.getValues;
return (
<form aria-label="agent form" onSubmit={methods.handleSubmit(mockSubmit)}>
<Controller
name="subagents"
control={methods.control}
render={({ field }) => (
<AgentSubagents field={field} currentAgentId="parent" maxSubagents={10} />
)}
/>
</form>
);
}
it('commits a selected agent before an immediate form submit', async () => {
render(<Harness />);
act(() => {
mockSelectAgent?.('child');
fireEvent.submit(screen.getByRole('form', { name: 'agent form' }));
});
expect(mockGetValues?.('subagents')).toEqual({
enabled: true,
allowSelf: false,
agent_ids: ['child'],
});
await waitFor(() =>
expect(mockSubmit).toHaveBeenCalledWith(
expect.objectContaining({
subagents: {
enabled: true,
allowSelf: false,
agent_ids: ['child'],
},
}),
expect.anything(),
),
);
});
});