feat: Agent Panel UI Enhancements (#7800)

* feat: add MCP Panel to Agent Builder

- Add MCP server panel and configuration UI
- Implement MCP input forms and tool lists
- Add MCP icon and metadata support
- Integrate MCP with agent configuration
- Add localization support for MCP features
- Refactor components for better reusability
- Update types and add MCP-related mutations
- Fix small issues with Actions and AgentSelect
- Refactor AgentPanelSwitch and related components to use new
  AgentPanelContext to reduce prop drilling

* chore: import order

* chore: clean up import statements and unused var in ActionsPanel component

* refactor: AgentPanelContext with actions query, remove unnecessary `actions` state

- Added actions query using `useGetActionsQuery` to fetch actions based on the current agent ID.
- Removed now unused `setActions` state and related logic from `AgentPanelContext` and `AgentPanelSwitch` components.
- Updated `AgentPanelContextType` to reflect the removal of `setActions`.

* chore: re-order import statements in AgentConfig component

* chore: re-order import statements in ModelPanel component

* chore: update ModelPanel props to consolidated props to avoid passing unnecessary props

* chore: update import statements in Providers index file to include ToastProvider and AgentPanelContext exports

* chore: clean up import statements in VersionPanel component

* refactor: streamline AgentConfig and AgentPanel components

- Consolidated props in AgentConfig to only include necessary fields.
- Updated AgentPanel to remove unused state and props, enhancing clarity and maintainability.
- Reorganized import statements for better structure and readability.

* refactor: replace default agent form values with utility function

- Updated AgentsProvider, AgentPanel, AgentSelect, and DeleteButton components to use getDefaultAgentFormValues utility function instead of directly importing defaultAgentFormValues.
- Enhanced the initialization of agent forms by incorporating localStorage values for model and provider in the new utility function.

* chore: comment out rendering MCPSection

---------

Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
This commit is contained in:
Danny Avila 2025-06-13 15:47:41 -04:00 committed by GitHub
parent 5f2d1c5dc9
commit 4419e2c294
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1027 additions and 136 deletions

View file

@ -1,12 +1,12 @@
import type { Agent, TAgentsEndpoint } from 'librechat-data-provider';
import { ChevronLeft } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import type { AgentPanelProps } from '~/common';
import { Panel } from '~/common';
import { useGetAgentByIdQuery, useRevertAgentVersionMutation } from '~/data-provider';
import type { Agent } from 'librechat-data-provider';
import { isActiveVersion } from './isActiveVersion';
import { useAgentPanelContext } from '~/Providers';
import { useLocalize, useToast } from '~/hooks';
import VersionContent from './VersionContent';
import { isActiveVersion } from './isActiveVersion';
import { Panel } from '~/common';
export type VersionRecord = Record<string, any>;
@ -39,15 +39,13 @@ export interface AgentWithVersions extends Agent {
versions?: Array<VersionRecord>;
}
export type VersionPanelProps = {
agentsConfig: TAgentsEndpoint | null;
setActivePanel: AgentPanelProps['setActivePanel'];
selectedAgentId?: string;
};
export default function VersionPanel({ setActivePanel, selectedAgentId = '' }: VersionPanelProps) {
export default function VersionPanel() {
const localize = useLocalize();
const { showToast } = useToast();
const { agent_id, setActivePanel } = useAgentPanelContext();
const selectedAgentId = agent_id ?? '';
const {
data: agent,
isLoading,

View file

@ -55,13 +55,18 @@ jest.mock('~/hooks', () => ({
useToast: jest.fn(() => ({ showToast: jest.fn() })),
}));
// Mock the AgentPanelContext
jest.mock('~/Providers/AgentPanelContext', () => ({
...jest.requireActual('~/Providers/AgentPanelContext'),
useAgentPanelContext: jest.fn(),
}));
describe('VersionPanel', () => {
const mockSetActivePanel = jest.fn();
const defaultProps = {
agentsConfig: null,
setActivePanel: mockSetActivePanel,
selectedAgentId: 'agent-123',
};
const mockUseAgentPanelContext = jest.requireMock(
'~/Providers/AgentPanelContext',
).useAgentPanelContext;
const mockUseGetAgentByIdQuery = jest.requireMock('~/data-provider').useGetAgentByIdQuery;
beforeEach(() => {
@ -72,10 +77,17 @@ describe('VersionPanel', () => {
error: null,
refetch: jest.fn(),
});
// Set up the default context mock
mockUseAgentPanelContext.mockReturnValue({
setActivePanel: mockSetActivePanel,
agent_id: 'agent-123',
activePanel: Panel.version,
});
});
test('renders panel UI and handles navigation', () => {
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(screen.getByText('com_ui_agent_version_history')).toBeInTheDocument();
expect(screen.getByTestId('version-content')).toBeInTheDocument();
@ -84,7 +96,7 @@ describe('VersionPanel', () => {
});
test('VersionContent receives correct props', () => {
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({
selectedAgentId: 'agent-123',
@ -101,19 +113,31 @@ describe('VersionPanel', () => {
});
test('handles data state variations', () => {
render(<VersionPanel {...defaultProps} selectedAgentId="" />);
// Test with empty agent_id
mockUseAgentPanelContext.mockReturnValueOnce({
setActivePanel: mockSetActivePanel,
agent_id: '',
activePanel: Panel.version,
});
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({ selectedAgentId: '' }),
expect.anything(),
);
// Test with null data
mockUseGetAgentByIdQuery.mockReturnValueOnce({
data: null,
isLoading: false,
error: null,
refetch: jest.fn(),
});
render(<VersionPanel {...defaultProps} />);
mockUseAgentPanelContext.mockReturnValueOnce({
setActivePanel: mockSetActivePanel,
agent_id: 'agent-123',
activePanel: Panel.version,
});
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({
versionContext: expect.objectContaining({
@ -125,13 +149,14 @@ describe('VersionPanel', () => {
expect.anything(),
);
// 3. versions is undefined
mockUseGetAgentByIdQuery.mockReturnValueOnce({
data: { ...mockAgentData, versions: undefined },
isLoading: false,
error: null,
refetch: jest.fn(),
});
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({
versionContext: expect.objectContaining({ versions: [] }),
@ -139,18 +164,20 @@ describe('VersionPanel', () => {
expect.anything(),
);
// 4. loading state
mockUseGetAgentByIdQuery.mockReturnValueOnce({
data: null,
isLoading: true,
error: null,
refetch: jest.fn(),
});
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({ isLoading: true }),
expect.anything(),
);
// 5. error state
const testError = new Error('Test error');
mockUseGetAgentByIdQuery.mockReturnValueOnce({
data: null,
@ -158,7 +185,7 @@ describe('VersionPanel', () => {
error: testError,
refetch: jest.fn(),
});
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({ error: testError }),
expect.anything(),
@ -173,7 +200,7 @@ describe('VersionPanel', () => {
refetch: jest.fn(),
});
render(<VersionPanel {...defaultProps} />);
render(<VersionPanel />);
expect(VersionContent).toHaveBeenCalledWith(
expect.objectContaining({
versionContext: expect.objectContaining({