🧼 fix: Clear Model Spec Fields on Configured Selections (#15544)

This commit is contained in:
Danny Avila 2026-09-03 09:46:15 -04:00 • committed by GitHub
parent 2d794a0439
commit ac55b5fad4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 228 additions and 11 deletions

View file

@ -17,8 +17,8 @@ import {
AgentListResponse,
} from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { renderAgentAvatar, clearMessagesCache, specDisplayFieldReset } from '~/utils';
import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks';
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
import { useChatContext } from '~/Providers';
interface SupportContact {
@ -79,6 +79,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
title: localize('com_agents_chat_with', { name: agent.name || localize('com_ui_agent') }),
...specDisplayFieldReset,
};
const currentConvo = getDefaultConversation({

View file

@ -11,8 +11,8 @@ import {
AgentListResponse,
} from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { renderAgentAvatar, clearMessagesCache, specDisplayFieldReset } from '~/utils';
import { useLocalize, useDefaultConvo, useFavorites } from '~/hooks';
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
import { useChatContext } from '~/Providers';
import AgentContact from './AgentContact';
@ -64,6 +64,7 @@ const AgentDetailContent: React.FC<AgentDetailContentProps> = ({ agent }) => {
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
title: localize('com_agents_chat_with', { name: agent.name || localize('com_ui_agent') }),
...specDisplayFieldReset,
};
const currentConvo = getDefaultConversation({

View file

@ -49,6 +49,15 @@ jest.mock('~/utils/agents', () => ({
)),
}));
jest.mock('~/utils/endpoints', () => ({
specDisplayFieldReset: {
spec: null,
iconURL: null,
modelLabel: null,
greeting: undefined,
},
}));
jest.mock('~/Providers', () => ({
useChatContext: jest.fn(),
}));
@ -260,10 +269,52 @@ describe('AgentDetail', () => {
endpoint: EModelEndpoint.agents,
agent_id: 'test-agent-id',
title: 'Chat with Test Agent',
spec: null,
iconURL: null,
modelLabel: null,
greeting: undefined,
},
});
});
it('should clear model spec display fields when starting an agent chat', async () => {
const user = userEvent.setup();
const mockNewConversation = jest.fn();
const { useChatContext } = require('~/Providers');
(useChatContext as jest.Mock).mockReturnValue({
conversation: {
conversationId: 'test-convo-id',
spec: 'ClickHouse Agent',
iconURL: '/images/clickhouse.svg',
modelLabel: 'ClickHouse Agent',
},
newConversation: mockNewConversation,
});
const { useDefaultConvo } = require('~/hooks');
(useDefaultConvo as jest.Mock).mockReturnValue(({ conversation, preset }) => ({
...conversation,
...preset,
}));
renderWithProviders(<AgentDetail {...defaultProps} />);
await user.click(screen.getByRole('button', { name: 'com_agents_start_chat' }));
expect(mockNewConversation).toHaveBeenCalledWith({
template: expect.objectContaining({
endpoint: EModelEndpoint.agents,
agent_id: 'test-agent-id',
spec: null,
iconURL: null,
modelLabel: null,
}),
preset: expect.objectContaining({
spec: null,
iconURL: null,
modelLabel: null,
}),
});
});
it('should not navigate when agent is null', async () => {
const user = userEvent.setup();
renderWithProviders(<AgentDetail {...defaultProps} agent={null as any} />);

View file

@ -1,9 +1,14 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { TConversation } from 'librechat-data-provider';
import '@testing-library/jest-dom';
import AgentDetailContent from '../AgentDetailContent';
const mockNewConversation = jest.fn();
let mockConversation: Partial<TConversation> | undefined;
jest.mock('librechat-data-provider', () => ({
QueryKeys: {
agents: 'agents',
@ -62,8 +67,8 @@ jest.mock('~/hooks', () => ({
jest.mock('~/Providers', () => ({
useChatContext: () => ({
conversation: undefined,
newConversation: jest.fn(),
conversation: mockConversation,
newConversation: mockNewConversation,
}),
}));
@ -71,6 +76,12 @@ jest.mock('~/utils', () => ({
cn: (...classes: string[]) => classes.filter(Boolean).join(' '),
clearMessagesCache: jest.fn(),
renderAgentAvatar: () => <div data-testid="agent-avatar" />,
specDisplayFieldReset: {
spec: null,
iconURL: null,
modelLabel: null,
greeting: undefined,
},
}));
const renderWithClient = (children: React.ReactNode) => {
@ -88,9 +99,14 @@ const baseAgent = {
provider: 'openai',
model: 'gpt-4',
model_parameters: {},
};
} as React.ComponentProps<typeof AgentDetailContent>['agent'];
describe('AgentDetailContent', () => {
beforeEach(() => {
jest.clearAllMocks();
mockConversation = undefined;
});
it('renders support contact with mailto link', () => {
renderWithClient(
<AgentDetailContent
@ -127,4 +143,32 @@ describe('AgentDetailContent', () => {
expect(screen.getByText('Owner User')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Owner User' })).not.toBeInTheDocument();
});
it('clears model spec display fields when starting an agent chat', async () => {
const user = userEvent.setup();
mockConversation = {
conversationId: 'existing-conversation',
spec: 'ClickHouse Agent',
iconURL: '/images/clickhouse.svg',
modelLabel: 'ClickHouse Agent',
};
renderWithClient(<AgentDetailContent agent={baseAgent} />);
await user.click(screen.getByRole('button', { name: 'Start chat' }));
expect(mockNewConversation).toHaveBeenCalledWith({
template: expect.objectContaining({
endpoint: 'agents',
agent_id: 'agent-1',
spec: null,
iconURL: null,
modelLabel: null,
}),
preset: expect.objectContaining({
spec: null,
iconURL: null,
modelLabel: null,
}),
});
});
});

View file

@ -30,7 +30,15 @@ jest.mock('~/Providers/AgentsMapContext', () => ({
useAgentsMapContext: jest.fn(() => ({ 'agent-1': { id: 'agent-1', name: 'Agent' } })),
}));
jest.mock('~/utils', () => ({ logger: { log: jest.fn() } }));
jest.mock('~/utils', () => ({
logger: { log: jest.fn() },
specDisplayFieldReset: {
spec: null,
iconURL: null,
modelLabel: null,
greeting: undefined,
},
}));
import useSelectAgent from '../useSelectAgent';
@ -39,7 +47,16 @@ describe('useSelectAgent', () => {
jest.clearAllMocks();
mockGetConversation.mockResolvedValue({ endpoint: EModelEndpoint.agents } as TConversation);
mockGetDefaultConversation.mockImplementation(
({ conversation }: { conversation: Partial<TConversation> }) => conversation,
({
conversation,
preset,
}: {
conversation: Partial<TConversation>;
preset: Partial<TConversation>;
}) => ({
...conversation,
...preset,
}),
);
});
@ -85,4 +102,28 @@ describe('useSelectAgent', () => {
expect(mockNewConversation.mock.calls[0][0].keepComposerState).toBe(false);
expect(mockNewConversation.mock.calls[1][0].keepComposerState).toBe(true);
});
it('clears model spec display fields when selecting an agent', async () => {
mockGetConversation.mockResolvedValue({
endpoint: EModelEndpoint.openAI,
spec: 'ClickHouse Agent',
iconURL: EModelEndpoint.bedrock,
modelLabel: 'ClickHouse Agent',
} as TConversation);
mockFetchQuery.mockResolvedValue({ id: 'agent-1', name: 'Full Agent' });
const { result } = renderHook(() => useSelectAgent());
await act(async () => {
await result.current.onSelect('agent-1');
});
const firstConversation = mockNewConversation.mock.calls[0][0].template;
expect(firstConversation).toMatchObject({
endpoint: EModelEndpoint.agents,
agent_id: 'agent-1',
spec: null,
iconURL: null,
modelLabel: null,
});
});
});

View file

@ -11,8 +11,8 @@ import type { TConversation, TPreset, Agent } from 'librechat-data-provider';
import useGetConversation from '~/hooks/Conversations/useGetConversation';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
import { logger, specDisplayFieldReset } from '~/utils';
import useNewConvo from '~/hooks/useNewConvo';
import { logger } from '~/utils';
export default function useSelectAgent() {
const queryClient = useQueryClient();
@ -40,7 +40,7 @@ export default function useSelectAgent() {
return;
}
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}), agent_id: agent.id },
conversation: { ...(conversation ?? {}), agent_id: agent.id, ...specDisplayFieldReset },
preset: template,
});
newConversation({
@ -63,6 +63,7 @@ export default function useSelectAgent() {
endpoint: EModelEndpoint.agents,
agent_id: agent.id,
conversationId: Constants.NEW_CONVO as string,
...specDisplayFieldReset,
};
await updateConversation({ id: agent.id }, template);

View file

@ -0,0 +1,77 @@
import { renderHook, act } from '@testing-library/react';
import { EModelEndpoint } from 'librechat-data-provider';
const mockNewConversation = jest.fn();
const mockGetDefaultConversation = jest.fn();
jest.mock('~/Providers/ChatContext', () => ({
useChatContext: jest.fn(() => ({
conversation: {
endpoint: 'assistants',
spec: 'Assistant Spec',
iconURL: '/images/assistant.svg',
modelLabel: 'Assistant Spec',
},
newConversation: mockNewConversation,
})),
}));
jest.mock('~/hooks/Conversations/useDefaultConvo', () => ({
__esModule: true,
default: jest.fn(() => mockGetDefaultConversation),
}));
jest.mock('../useAssistantListMap', () => ({
__esModule: true,
default: jest.fn(() => ({
assistants: {
'assistant-1': { id: 'assistant-1', model: 'gpt-4' },
},
})),
}));
jest.mock('~/utils', () => ({
logger: { log: jest.fn() },
mapAssistants: jest.fn(),
specDisplayFieldReset: {
spec: null,
iconURL: null,
modelLabel: null,
greeting: undefined,
},
}));
import useSelectAssistant from '../useSelectAssistant';
describe('useSelectAssistant', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetDefaultConversation.mockImplementation(({ conversation, preset }) => ({
...conversation,
...preset,
}));
});
it('clears model spec display fields when selecting an assistant', () => {
const { result } = renderHook(() => useSelectAssistant(EModelEndpoint.assistants));
act(() => {
result.current.onSelect('assistant-1');
});
expect(mockNewConversation).toHaveBeenCalledWith({
template: expect.objectContaining({
endpoint: EModelEndpoint.assistants,
assistant_id: 'assistant-1',
spec: null,
iconURL: null,
modelLabel: null,
}),
preset: expect.objectContaining({
spec: null,
iconURL: null,
modelLabel: null,
}),
});
});
});

View file

@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantsEndpoint, TConversation, TPreset } from 'librechat-data-provider';
import { mapAssistants, logger, specDisplayFieldReset } from '~/utils';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useChatContext } from '~/Providers/ChatContext';
import useAssistantListMap from './useAssistantListMap';
import { mapAssistants, logger } from '~/utils';
export default function useSelectAssistant(endpoint: AssistantsEndpoint) {
const getDefaultConversation = useDefaultConvo();
@ -22,12 +22,13 @@ export default function useSelectAssistant(endpoint: AssistantsEndpoint) {
assistant_id: assistant.id,
model: assistant.model,
conversationId: 'new',
...specDisplayFieldReset,
};
logger.log('conversation', 'Updating conversation with assistant', assistant);
if (isAssistantsEndpoint(conversation?.endpoint)) {
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}) },
conversation: { ...(conversation ?? {}), ...specDisplayFieldReset },
preset: template,
});
newConversation({