🤖 refactor: Side Panel Agent UI To Account For Ephemeral Agents (#9763)

* refactor: Remove unused imports and consolidate ephemeral agent logic

* refactor: Side Panel agent handling to account for ephemeral agents for UI

* refactor: Replace Constants.EPHEMERAL_AGENT_ID checks with isEphemeralAgent utility for consistency

* ci: AgentPanel tests with additional mock configurations and utility functions
This commit is contained in:
Danny Avila 2025-09-22 09:48:05 -04:00 committed by GitHub
parent a6bf2b6ce3
commit 8a60e8990f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 87 additions and 77 deletions

View file

@ -1,27 +1,26 @@
import { useEffect } from 'react';
import { ChevronLeft } from 'lucide-react';
import { useForm, FormProvider } from 'react-hook-form';
import {
AuthTypeEnum,
AuthorizationTypeEnum,
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import {
OGDialogTemplate,
TrashIcon,
OGDialog,
OGDialogTrigger,
Label,
OGDialog,
TrashIcon,
OGDialogTrigger,
useToastContext,
OGDialogTemplate,
} from '@librechat/client';
import type { ActionAuthForm } from '~/common';
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
import { useDeleteAgentAction } from '~/data-provider';
import type { ActionAuthForm } from '~/common';
import { Panel, isEphemeralAgent } from '~/common';
import ActionsInput from './ActionsInput';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function ActionsPanel() {
const localize = useLocalize();
@ -109,7 +108,7 @@ export default function ActionsPanel() {
<div className="absolute right-0 top-6">
<button
type="button"
disabled={!agent_id || !action.action_id}
disabled={isEphemeralAgent(agent_id) || !action.action_id}
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
>
<TrashIcon className="text-red-500" />
@ -127,7 +126,7 @@ export default function ActionsPanel() {
}
selection={{
selectHandler: () => {
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
return showToast({
message: localize('com_agents_no_agent_id_error'),
status: 'error',
@ -135,7 +134,7 @@ export default function ActionsPanel() {
}
deleteAgentAction.mutate({
action_id: action.action_id,
agent_id,
agent_id: agent_id || '',
});
},
selectClasses:

View file

@ -18,6 +18,7 @@ import { useFileMapContext, useAgentPanelContext } from '~/Providers';
import AgentCategorySelector from './AgentCategorySelector';
import Action from '~/components/SidePanel/Builder/Action';
import { useLocalize, useVisibleTools } from '~/hooks';
import { Panel, isEphemeralAgent } from '~/common';
import { useGetAgentFiles } from '~/data-provider';
import { icons } from '~/hooks/Endpoint/Icons';
import Instructions from './Instructions';
@ -29,7 +30,6 @@ import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
import MCPTools from './MCPTools';
import { Panel } from '~/common';
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
@ -149,7 +149,7 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
}, [agent, agent_id, mergedFileMap]);
const handleAddActions = useCallback(() => {
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
showToast({
message: localize('com_assistants_actions_disabled'),
status: 'warning',
@ -370,7 +370,7 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
{(actionsEnabled ?? false) && (
<button
type="button"
disabled={!agent_id}
disabled={isEphemeralAgent(agent_id)}
onClick={handleAddActions}
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
aria-haspopup="dialog"

View file

@ -37,22 +37,28 @@ jest.mock('librechat-data-provider', () => {
dataService: {
updateAgent: jest.fn(),
},
Tools: {
Tools: actualModule.Tools || {
execute_code: 'execute_code',
file_search: 'file_search',
web_search: 'web_search',
},
Constants: {
Constants: actualModule.Constants || {
EPHEMERAL_AGENT_ID: 'ephemeral',
},
SystemRoles: {
SystemRoles: actualModule.SystemRoles || {
ADMIN: 'ADMIN',
},
EModelEndpoint: {
EModelEndpoint: actualModule.EModelEndpoint || {
agents: 'agents',
chatGPTBrowser: 'chatGPTBrowser',
gptPlugins: 'gptPlugins',
},
ResourceType: actualModule.ResourceType || {
AGENT: 'agent',
},
PermissionBits: actualModule.PermissionBits || {
EDIT: 2,
},
isAssistantsEndpoint: jest.fn(() => false),
};
});
@ -97,6 +103,13 @@ jest.mock('~/hooks', () => ({
useAuthContext: () => ({ user: { id: 'user-123', role: 'USER' } }),
}));
jest.mock('~/hooks/useResourcePermissions', () => ({
useResourcePermissions: () => ({
hasPermission: jest.fn(() => true),
isLoading: false,
}),
}));
jest.mock('~/Providers/AgentPanelContext', () => ({
useAgentPanelContext: () => ({
activePanel: 'builder',
@ -109,6 +122,9 @@ jest.mock('~/Providers/AgentPanelContext', () => ({
}));
jest.mock('~/common', () => ({
isEphemeralAgent: (agentId: string | null | undefined): boolean => {
return agentId == null || agentId === '' || agentId === 'ephemeral';
},
Panel: {
model: 'model',
builder: 'builder',
@ -199,6 +215,10 @@ jest.mock('~/data-provider', () => {
return {
...actual,
useGetAgentByIdQuery: jest.fn(),
useGetExpandedAgentByIdQuery: jest.fn(() => ({
data: null,
isInitialLoading: false,
})),
useUpdateAgentMutation: actual.useUpdateAgentMutation,
};
});

View file

@ -5,7 +5,6 @@ import { useWatch, useForm, FormProvider } from 'react-hook-form';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
Tools,
Constants,
SystemRoles,
ResourceType,
EModelEndpoint,
@ -25,11 +24,11 @@ import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import AdvancedPanel from './Advanced/AdvancedPanel';
import { Panel, isEphemeralAgent } from '~/common';
import AgentConfig from './AgentConfig';
import AgentSelect from './AgentSelect';
import AgentFooter from './AgentFooter';
import ModelPanel from './ModelPanel';
import { Panel } from '~/common';
export default function AgentPanel() {
const localize = useLocalize();
@ -57,11 +56,7 @@ export default function AgentPanel() {
const canEdit = hasPermission(PermissionBits.EDIT);
const expandedAgentQuery = useGetExpandedAgentByIdQuery(current_agent_id ?? '', {
enabled:
!!(current_agent_id ?? '') &&
current_agent_id !== Constants.EPHEMERAL_AGENT_ID &&
canEdit &&
!permissionsLoading,
enabled: !isEphemeralAgent(current_agent_id) && canEdit && !permissionsLoading,
});
const agentQuery = canEdit && expandedAgentQuery.data ? expandedAgentQuery : basicAgentQuery;
@ -298,7 +293,7 @@ export default function AgentPanel() {
</Button>
<Button
variant="submit"
disabled={!agent_id || agentQuery.isInitialLoading}
disabled={isEphemeralAgent(agent_id) || agentQuery.isInitialLoading}
onClick={(e) => {
e.preventDefault();
handleSelectAgent();

View file

@ -1,11 +1,11 @@
import { useEffect } from 'react';
import { AgentPanelProvider, useAgentPanelContext } from '~/Providers/AgentPanelContext';
import { Panel, isEphemeralAgent } from '~/common';
import VersionPanel from './Version/VersionPanel';
import { useChatContext } from '~/Providers';
import ActionsPanel from './ActionsPanel';
import AgentPanel from './AgentPanel';
import MCPPanel from './MCPPanel';
import { Panel } from '~/common';
export default function AgentPanelSwitch() {
return (
@ -21,7 +21,7 @@ function AgentPanelSwitchWithContext() {
useEffect(() => {
const agent_id = conversation?.agent_id ?? '';
if (agent_id) {
if (!isEphemeralAgent(agent_id)) {
setCurrentAgentId(agent_id);
}
}, [setCurrentAgentId, conversation?.agent_id]);

View file

@ -14,6 +14,7 @@ import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { useChatContext } from '~/Providers';
import { isEphemeralAgent } from '~/common';
const tool_resource = EToolResources.execute_code;
@ -85,7 +86,7 @@ export default function Files({
<div>
<button
type="button"
disabled={!agent_id || codeChecked === false}
disabled={isEphemeralAgent(agent_id) || codeChecked === false}
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
@ -96,7 +97,7 @@ export default function Files({
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!agent_id || codeChecked === false}
disabled={isEphemeralAgent(agent_id) || codeChecked === false}
onChange={handleFileChange}
/>
<AttachmentIcon className="text-token-text-primary h-4 w-4" />

View file

@ -14,6 +14,7 @@ import { logger, getDefaultAgentFormValues } from '~/utils';
import { useLocalize, useSetIndexOptions } from '~/hooks';
import { useDeleteAgentMutation } from '~/data-provider';
import { useChatContext } from '~/Providers';
import { isEphemeralAgent } from '~/common';
export default function DeleteButton({
agent_id,
@ -76,7 +77,7 @@ export default function DeleteButton({
},
});
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
return null;
}

View file

@ -1,6 +1,7 @@
import { CopyIcon } from 'lucide-react';
import { useToastContext, Button } from '@librechat/client';
import { useDuplicateAgentMutation } from '~/data-provider';
import { isEphemeralAgent } from '~/common';
import { useLocalize } from '~/hooks';
export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
@ -23,7 +24,7 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
},
});
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
return null;
}

View file

@ -22,8 +22,8 @@ import { useFileHandling, useLocalize, useLazyEffect, useSharePointFileHandling
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
import { SharePointPickerDialog } from '~/components/SharePoint';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { ESide, isEphemeralAgent } from '~/common';
import { useChatContext } from '~/Providers';
import { ESide } from '~/common';
export default function FileContext({
agent_id,
@ -156,7 +156,7 @@ export default function FileContext({
) : (
<button
type="button"
disabled={!agent_id}
disabled={isEphemeralAgent(agent_id)}
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleLocalFileClick}
>
@ -173,7 +173,7 @@ export default function FileContext({
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!agent_id}
disabled={isEphemeralAgent(agent_id)}
onChange={handleFileChange}
/>
</div>

View file

@ -18,6 +18,7 @@ import { SharePointPickerDialog } from '~/components/SharePoint';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import FileSearchCheckbox from './FileSearchCheckbox';
import { useChatContext } from '~/Providers';
import { isEphemeralAgent } from '~/common';
export default function FileSearch({
agent_id,
@ -69,7 +70,7 @@ export default function FileSearch({
const isUploadDisabled = endpointFileConfig.disabled ?? false;
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
const disabledUploadButton = !agent_id || fileSearchChecked === false;
const disabledUploadButton = isEphemeralAgent(agent_id) || fileSearchChecked === false;
const handleSharePointFilesSelected = async (sharePointFiles: any[]) => {
try {

View file

@ -7,21 +7,19 @@ import {
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import {
OGDialog,
OGDialogTrigger,
Label,
OGDialogTemplate,
OGDialog,
TrashIcon,
OGDialogTrigger,
useToastContext,
OGDialogTemplate,
} from '@librechat/client';
import type { MCPForm } from '~/common';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
import { defaultMCPFormValues } from '~/common/mcp';
import type { MCPForm } from '~/common';
import { Panel, isEphemeralAgent } from '~/common';
import { useLocalize } from '~/hooks';
import MCPInput from './MCPInput';
import { Panel } from '~/common';
// TODO: Add MCP delete (for now mocked for ui)
// import { useDeleteAgentMCP } from '~/data-provider';
function useDeleteAgentMCP({
onSuccess,
@ -127,7 +125,7 @@ export default function MCPPanel() {
<div className="absolute right-0 top-6">
<button
type="button"
disabled={!agent_id || !mcp.mcp_id}
disabled={isEphemeralAgent(agent_id) || !mcp.mcp_id}
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
>
<TrashIcon className="text-red-500" />
@ -145,7 +143,7 @@ export default function MCPPanel() {
}
selection={{
selectHandler: () => {
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
return showToast({
message: localize('com_agents_no_agent_id_error'),
status: 'error',
@ -153,7 +151,7 @@ export default function MCPPanel() {
}
deleteAgentMCP.mutate({
mcp_id: mcp.mcp_id,
agent_id,
agent_id: agent_id || '',
});
},
selectClasses:

View file

@ -3,15 +3,15 @@ import { useLocalize } from '~/hooks';
import { useToastContext } from '@librechat/client';
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
import MCP from '~/components/SidePanel/Builder/MCP';
import { Panel } from '~/common';
import { Panel, isEphemeralAgent } from '~/common';
export default function MCPSection() {
const { showToast } = useToastContext();
const localize = useLocalize();
const { showToast } = useToastContext();
const { mcps = [], agent_id, setMcp, setActivePanel } = useAgentPanelContext();
const handleAddMCP = useCallback(() => {
if (!agent_id) {
if (isEphemeralAgent(agent_id)) {
showToast({
message: localize('com_agents_mcps_disabled'),
status: 'warning',