🦥 perf: Lazy-Load Agent Version History in Editor (#13977)

Opening the agent editor fetched the full `versions` array (each a complete
config snapshot) alongside the agent, so agents with large histories were slow
to open. Version history is now loaded only when the user opens it.

- Add `getAgentWithVersionCount` (aggregation: version count, no versions array)
  and `getAgentVersions` data-schemas methods.
- `getAgentHandler` returns the version count without the heavy array; add
  `GET /agents/:id/versions` (EDIT-gated) for lazy retrieval.
- Add `useGetAgentVersionsQuery`; VersionPanel reads current config from the
  cached expanded query and fetches versions on open. Revert keeps the expanded
  cache and versions query in sync.
This commit is contained in:
Danny Avila 2026-06-26 12:19:54 -04:00 committed by GitHub
parent b15d40e3e4
commit 12fea693bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 301 additions and 45 deletions

View file

@ -1,8 +1,12 @@
import { ChevronLeft } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { ChevronLeft } from 'lucide-react';
import { useToastContext } from '@librechat/client';
import { useGetAgentByIdQuery, useRevertAgentVersionMutation } from '~/data-provider';
import type { AgentWithVersions, VersionContext } from './types';
import type { AgentWithVersions, VersionContext, VersionRecord } from './types';
import {
useGetAgentVersionsQuery,
useRevertAgentVersionMutation,
useGetExpandedAgentByIdQuery,
} from '~/data-provider';
import { isActiveVersion } from './isActiveVersion';
import { useAgentPanelContext } from '~/Providers';
import VersionContent from './VersionContent';
@ -16,7 +20,15 @@ export default function VersionPanel() {
const selectedAgentId = agent_id ?? '';
const { data: agent, isLoading, error, refetch } = useGetAgentByIdQuery(selectedAgentId);
const { data: agent } = useGetExpandedAgentByIdQuery(selectedAgentId, {
enabled: !!selectedAgentId,
});
const {
data: versionsData,
isLoading,
error,
refetch,
} = useGetAgentVersionsQuery(selectedAgentId);
const revertAgentVersion = useRevertAgentVersionMutation({
onSuccess: () => {
@ -34,7 +46,7 @@ export default function VersionPanel() {
},
});
const agentWithVersions = agent as AgentWithVersions;
const agentWithVersions = agent as AgentWithVersions | undefined;
const currentAgent = useMemo(() => {
if (!agentWithVersions) return null;
@ -48,14 +60,15 @@ export default function VersionPanel() {
};
}, [agentWithVersions]);
const versionRecords = useMemo<VersionRecord[]>(() => versionsData ?? [], [versionsData]);
const versions = useMemo(() => {
const versionsCopy = [...(agentWithVersions?.versions || [])];
return versionsCopy.sort((a, b) => {
return [...versionRecords].sort((a, b) => {
const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
return bTime - aTime;
});
}, [agentWithVersions?.versions]);
}, [versionRecords]);
const activeVersion = useMemo(() => {
return versions.length > 0
@ -73,7 +86,7 @@ export default function VersionPanel() {
return versions.map((version, displayIndex) => {
const originalIndex =
agentWithVersions?.versions?.findIndex(
versionRecords.findIndex(
(v) =>
v.updatedAt === version.updatedAt &&
v.createdAt === version.createdAt &&
@ -87,7 +100,7 @@ export default function VersionPanel() {
isActive: displayIndex === activeVersionId,
};
});
}, [versions, currentAgent, agentWithVersions?.versions]);
}, [versions, currentAgent, versionRecords]);
const versionContext: VersionContext = useMemo(
() => ({

View file

@ -1,8 +1,8 @@
import '@testing-library/jest-dom/extend-expect';
import { fireEvent, render, screen } from '@testing-library/react';
import { Panel } from '~/common/types';
import VersionContent from '../VersionContent';
import VersionPanel from '../VersionPanel';
import { Panel } from '~/common/types';
const mockAgentData = {
name: 'Test Agent',
@ -10,35 +10,42 @@ const mockAgentData = {
instructions: 'Test Instructions',
tools: ['tool1', 'tool2'],
capabilities: ['capability1', 'capability2'],
versions: [
{
name: 'Version 1',
description: 'Description 1',
instructions: 'Instructions 1',
tools: ['tool1'],
capabilities: ['capability1'],
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
{
name: 'Version 2',
description: 'Description 2',
instructions: 'Instructions 2',
tools: ['tool1', 'tool2'],
capabilities: ['capability1', 'capability2'],
createdAt: '2023-01-02T00:00:00Z',
updatedAt: '2023-01-02T00:00:00Z',
},
],
};
const mockVersions = [
{
name: 'Version 1',
description: 'Description 1',
instructions: 'Instructions 1',
tools: ['tool1'],
capabilities: ['capability1'],
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
{
name: 'Version 2',
description: 'Description 2',
instructions: 'Instructions 2',
tools: ['tool1', 'tool2'],
capabilities: ['capability1', 'capability2'],
createdAt: '2023-01-02T00:00:00Z',
updatedAt: '2023-01-02T00:00:00Z',
},
];
jest.mock('~/data-provider', () => ({
useGetAgentByIdQuery: jest.fn(() => ({
useGetExpandedAgentByIdQuery: jest.fn(() => ({
data: mockAgentData,
isLoading: false,
error: null,
refetch: jest.fn(),
})),
useGetAgentVersionsQuery: jest.fn(() => ({
data: mockVersions,
isLoading: false,
error: null,
refetch: jest.fn(),
})),
useRevertAgentVersionMutation: jest.fn(() => ({
mutate: jest.fn(),
isLoading: false,
@ -67,16 +74,24 @@ describe('VersionPanel', () => {
'~/Providers/AgentPanelContext',
).useAgentPanelContext;
const mockUseGetAgentByIdQuery = jest.requireMock('~/data-provider').useGetAgentByIdQuery;
const mockUseGetExpandedAgentByIdQuery =
jest.requireMock('~/data-provider').useGetExpandedAgentByIdQuery;
const mockUseGetAgentVersionsQuery = jest.requireMock('~/data-provider').useGetAgentVersionsQuery;
beforeEach(() => {
jest.clearAllMocks();
mockUseGetAgentByIdQuery.mockReturnValue({
mockUseGetExpandedAgentByIdQuery.mockReturnValue({
data: mockAgentData,
isLoading: false,
error: null,
refetch: jest.fn(),
});
mockUseGetAgentVersionsQuery.mockReturnValue({
data: mockVersions,
isLoading: false,
error: null,
refetch: jest.fn(),
});
// Set up the default context mock
mockUseAgentPanelContext.mockReturnValue({
@ -126,7 +141,13 @@ describe('VersionPanel', () => {
);
// Test with null data
mockUseGetAgentByIdQuery.mockReturnValueOnce({
mockUseGetExpandedAgentByIdQuery.mockReturnValueOnce({
data: null,
isLoading: false,
error: null,
refetch: jest.fn(),
});
mockUseGetAgentVersionsQuery.mockReturnValueOnce({
data: null,
isLoading: false,
error: null,
@ -150,8 +171,8 @@ describe('VersionPanel', () => {
);
// 3. versions is undefined
mockUseGetAgentByIdQuery.mockReturnValueOnce({
data: { ...mockAgentData, versions: undefined },
mockUseGetAgentVersionsQuery.mockReturnValueOnce({
data: undefined,
isLoading: false,
error: null,
refetch: jest.fn(),
@ -165,7 +186,7 @@ describe('VersionPanel', () => {
);
// 4. loading state
mockUseGetAgentByIdQuery.mockReturnValueOnce({
mockUseGetAgentVersionsQuery.mockReturnValueOnce({
data: null,
isLoading: true,
error: null,
@ -179,7 +200,7 @@ describe('VersionPanel', () => {
// 5. error state
const testError = new Error('Test error');
mockUseGetAgentByIdQuery.mockReturnValueOnce({
mockUseGetAgentVersionsQuery.mockReturnValueOnce({
data: null,
isLoading: false,
error: testError,
@ -193,12 +214,18 @@ describe('VersionPanel', () => {
});
test('memoizes agent data correctly', () => {
mockUseGetAgentByIdQuery.mockReturnValueOnce({
mockUseGetExpandedAgentByIdQuery.mockReturnValueOnce({
data: mockAgentData,
isLoading: false,
error: null,
refetch: jest.fn(),
});
mockUseGetAgentVersionsQuery.mockReturnValueOnce({
data: mockVersions,
isLoading: false,
error: null,
refetch: jest.fn(),
});
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(

View file

@ -379,6 +379,11 @@ export const useRevertAgentVersionMutation = (
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (revertedAgent, variables, context) => {
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], revertedAgent);
queryClient.setQueryData<t.Agent>(
[QueryKeys.agent, variables.agent_id, 'expanded'],
revertedAgent,
);
queryClient.invalidateQueries([QueryKeys.agent, variables.agent_id, 'versions']);
((keys: t.AgentListParams[]) => {
keys.forEach((key) => {

View file

@ -133,6 +133,33 @@ export const useGetExpandedAgentByIdQuery = (
);
};
/**
* Hook for lazily retrieving an agent's version history (EDIT permission).
* Only fetched when the user opens version history, so editors with large
* histories don't pay the cost on every open.
*/
export const useGetAgentVersionsQuery = (
agent_id: string | null | undefined,
config?: UseQueryOptions<t.Agent[]>,
): QueryObserverResult<t.Agent[]> => {
const isValidAgentId = !!agent_id && !isEphemeralAgent(agent_id);
return useQuery<t.Agent[]>(
[QueryKeys.agent, agent_id, 'versions'],
() =>
dataService.getAgentVersions({
agent_id: agent_id as string,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
...config,
enabled: isValidAgentId && (config?.enabled ?? true),
},
);
};
/**
* MARKETPLACE
*/