- {(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
+ {isSubmitting && showStopButton ? (
) : (
endpoint && (
diff --git a/client/src/components/Chat/Input/Files/AttachFile.tsx b/client/src/components/Chat/Input/Files/AttachFile.tsx
index c896109abb..38a3fa8c6f 100644
--- a/client/src/components/Chat/Input/Files/AttachFile.tsx
+++ b/client/src/components/Chat/Input/Files/AttachFile.tsx
@@ -22,7 +22,7 @@ const AttachFile = ({ disabled }: { disabled?: boolean | null }) => {
aria-label={localize('com_sidepanel_attach_files')}
disabled={isUploadDisabled}
className={cn(
- 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
+ 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
)}
onKeyDownCapture={(e) => {
if (!inputRef.current) {
diff --git a/client/src/components/Chat/Input/Files/AttachFileChat.tsx b/client/src/components/Chat/Input/Files/AttachFileChat.tsx
index 90ac3145bf..37b3584d3e 100644
--- a/client/src/components/Chat/Input/Files/AttachFileChat.tsx
+++ b/client/src/components/Chat/Input/Files/AttachFileChat.tsx
@@ -10,7 +10,8 @@ import {
getEndpointFileConfig,
} from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
-import { useGetFileConfig, useGetEndpointsQuery } from '~/data-provider';
+import { useGetFileConfig, useGetEndpointsQuery, useGetAgentByIdQuery } from '~/data-provider';
+import { useAgentsMapContext } from '~/Providers';
import AttachFileMenu from './AttachFileMenu';
import AttachFile from './AttachFile';
@@ -26,6 +27,28 @@ function AttachFileChat({
const isAgents = useMemo(() => isAgentsEndpoint(endpoint), [endpoint]);
const isAssistants = useMemo(() => isAssistantsEndpoint(endpoint), [endpoint]);
+ const agentsMap = useAgentsMapContext();
+
+ const needsAgentFetch = useMemo(() => {
+ if (!isAgents || !conversation?.agent_id) {
+ return false;
+ }
+ const agent = agentsMap?.[conversation.agent_id];
+ return !agent?.model_parameters;
+ }, [isAgents, conversation?.agent_id, agentsMap]);
+
+ const { data: agentData } = useGetAgentByIdQuery(conversation?.agent_id, {
+ enabled: needsAgentFetch,
+ });
+
+ const useResponsesApi = useMemo(() => {
+ if (!isAgents || !conversation?.agent_id || conversation?.useResponsesApi) {
+ return conversation?.useResponsesApi;
+ }
+ const agent = agentData || agentsMap?.[conversation.agent_id];
+ return agent?.model_parameters?.useResponsesApi;
+ }, [isAgents, conversation?.agent_id, conversation?.useResponsesApi, agentData, agentsMap]);
+
const { data: fileConfig = null } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
@@ -68,6 +91,7 @@ function AttachFileChat({
conversationId={conversationId}
agentId={conversation?.agent_id}
endpointFileConfig={endpointFileConfig}
+ useResponsesApi={useResponsesApi}
/>
);
}
diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx
index f34303047a..8c63953d5f 100644
--- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx
+++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx
@@ -9,6 +9,7 @@ import {
TerminalSquareIcon,
} from 'lucide-react';
import {
+ Providers,
EToolResources,
EModelEndpoint,
defaultAgentCapabilities,
@@ -36,6 +37,8 @@ import { ephemeralAgentByConvoId } from '~/store';
import { MenuItemProps } from '~/common';
import { cn } from '~/utils';
+type FileUploadType = 'image' | 'document' | 'image_document' | 'image_document_video_audio';
+
interface AttachFileMenuProps {
agentId?: string | null;
endpoint?: string | null;
@@ -43,6 +46,7 @@ interface AttachFileMenuProps {
conversationId: string;
endpointType?: EModelEndpoint;
endpointFileConfig?: EndpointFileConfig;
+ useResponsesApi?: boolean;
}
const AttachFileMenu = ({
@@ -52,6 +56,7 @@ const AttachFileMenu = ({
endpointType,
conversationId,
endpointFileConfig,
+ useResponsesApi,
}: AttachFileMenuProps) => {
const localize = useLocalize();
const isUploadDisabled = disabled ?? false;
@@ -83,9 +88,7 @@ const AttachFileMenu = ({
ephemeralAgent,
);
- const handleUploadClick = (
- fileType?: 'image' | 'document' | 'multimodal' | 'google_multimodal',
- ) => {
+ const handleUploadClick = (fileType?: FileUploadType) => {
if (!inputRef.current) {
return;
}
@@ -94,9 +97,9 @@ const AttachFileMenu = ({
inputRef.current.accept = 'image/*';
} else if (fileType === 'document') {
inputRef.current.accept = '.pdf,application/pdf';
- } else if (fileType === 'multimodal') {
+ } else if (fileType === 'image_document') {
inputRef.current.accept = 'image/*,.pdf,application/pdf';
- } else if (fileType === 'google_multimodal') {
+ } else if (fileType === 'image_document_video_audio') {
inputRef.current.accept = 'image/*,.pdf,application/pdf,video/*,audio/*';
} else {
inputRef.current.accept = '';
@@ -106,23 +109,33 @@ const AttachFileMenu = ({
};
const dropdownItems = useMemo(() => {
- const createMenuItems = (
- onAction: (fileType?: 'image' | 'document' | 'multimodal' | 'google_multimodal') => void,
- ) => {
+ const createMenuItems = (onAction: (fileType?: FileUploadType) => void) => {
const items: MenuItemProps[] = [];
- const currentProvider = provider || endpoint;
+ let currentProvider = provider || endpoint;
+
+ // This will be removed in a future PR to formally normalize Providers comparisons to be case insensitive
+ if (currentProvider?.toLowerCase() === Providers.OPENROUTER) {
+ currentProvider = Providers.OPENROUTER;
+ }
+
+ const isAzureWithResponsesApi =
+ currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi;
+
if (
isDocumentSupportedProvider(endpointType) ||
- isDocumentSupportedProvider(currentProvider)
+ isDocumentSupportedProvider(currentProvider) ||
+ isAzureWithResponsesApi
) {
items.push({
label: localize('com_ui_upload_provider'),
onClick: () => {
setToolResource(undefined);
- onAction(
- (provider || endpoint) === EModelEndpoint.google ? 'google_multimodal' : 'multimodal',
- );
+ let fileType: Exclude
= 'image_document';
+ if (currentProvider === Providers.GOOGLE || currentProvider === Providers.OPENROUTER) {
+ fileType = 'image_document_video_audio';
+ }
+ onAction(fileType);
},
icon: ,
});
@@ -204,6 +217,7 @@ const AttachFileMenu = ({
provider,
endpointType,
capabilities,
+ useResponsesApi,
setToolResource,
setEphemeralAgent,
sharePointEnabled,
@@ -220,7 +234,7 @@ const AttachFileMenu = ({
id="attach-file-menu-button"
aria-label="Attach File Options"
className={cn(
- 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
+ 'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
isPopoverActive && 'bg-surface-hover',
)}
>
diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx
index eb5f86d3b9..a59a7e3e9d 100644
--- a/client/src/components/Chat/Input/Files/DragDropModal.tsx
+++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { OGDialog, OGDialogTemplate } from '@librechat/client';
import {
+ Providers,
inferMimeType,
EToolResources,
EModelEndpoint,
@@ -46,7 +47,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
* Use definition for agents endpoint for ephemeral agents
* */
const capabilities = useAgentCapabilities(agentsConfig?.capabilities ?? defaultAgentCapabilities);
- const { conversationId, agentId, endpoint, endpointType } = useDragDropContext();
+ const { conversationId, agentId, endpoint, endpointType, useResponsesApi } = useDragDropContext();
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(conversationId ?? ''));
const { fileSearchAllowedByAgent, codeAllowedByAgent, provider } = useAgentToolPermissions(
agentId,
@@ -55,15 +56,28 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
const options = useMemo(() => {
const _options: FileOption[] = [];
- const currentProvider = provider || endpoint;
+ let currentProvider = provider || endpoint;
+
+ // This will be removed in a future PR to formally normalize Providers comparisons to be case insensitive
+ if (currentProvider?.toLowerCase() === Providers.OPENROUTER) {
+ currentProvider = Providers.OPENROUTER;
+ }
/** Helper to get inferred MIME type for a file */
const getFileType = (file: File) => inferMimeType(file.name, file.type);
+ const isAzureWithResponsesApi =
+ currentProvider === EModelEndpoint.azureOpenAI && useResponsesApi;
+
// Check if provider supports document upload
- if (isDocumentSupportedProvider(endpointType) || isDocumentSupportedProvider(currentProvider)) {
- const isGoogleProvider = currentProvider === EModelEndpoint.google;
- const validFileTypes = isGoogleProvider
+ if (
+ isDocumentSupportedProvider(endpointType) ||
+ isDocumentSupportedProvider(currentProvider) ||
+ isAzureWithResponsesApi
+ ) {
+ const supportsImageDocVideoAudio =
+ currentProvider === EModelEndpoint.google || currentProvider === Providers.OPENROUTER;
+ const validFileTypes = supportsImageDocVideoAudio
? files.every((file) => {
const type = getFileType(file);
return (
@@ -123,6 +137,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
endpoint,
endpointType,
capabilities,
+ useResponsesApi,
codeAllowedByAgent,
fileSearchAllowedByAgent,
]);
diff --git a/client/src/components/Chat/Input/Files/FilesView.tsx b/client/src/components/Chat/Input/Files/MyFilesModal.tsx
similarity index 78%
rename from client/src/components/Chat/Input/Files/FilesView.tsx
rename to client/src/components/Chat/Input/Files/MyFilesModal.tsx
index 13a378b93d..098b738b33 100644
--- a/client/src/components/Chat/Input/Files/FilesView.tsx
+++ b/client/src/components/Chat/Input/Files/MyFilesModal.tsx
@@ -5,7 +5,15 @@ import { useGetFiles } from '~/data-provider';
import { DataTable, columns } from './Table';
import { useLocalize } from '~/hooks';
-export default function Files({ open, onOpenChange }) {
+export function MyFilesModal({
+ open,
+ onOpenChange,
+ triggerRef,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ triggerRef?: React.RefObject;
+}) {
const localize = useLocalize();
const { data: files = [] } = useGetFiles({
@@ -18,7 +26,7 @@ export default function Files({ open, onOpenChange }) {
});
return (
-
+
[] = [
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
aria-sort={ariaSort}
- aria-label={localize('com_ui_name_sort')} aria-hidden="true"
+ aria-label={localize('com_ui_name_sort')}
+ aria-hidden="true"
aria-current={sortState ? 'true' : 'false'}
>
{localize('com_ui_name')}
@@ -150,7 +151,8 @@ export const columns: ColumnDef[] = [
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
aria-sort={ariaSort}
- aria-label={localize('com_ui_date_sort')} aria-hidden="true"
+ aria-label={localize('com_ui_date_sort')}
+ aria-hidden="true"
aria-current={sortState ? 'true' : 'false'}
>
{localize('com_ui_date')}
@@ -268,7 +270,8 @@ export const columns: ColumnDef[] = [
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
aria-sort={ariaSort}
- aria-label={localize('com_ui_size_sort')} aria-hidden="true"
+ aria-label={localize('com_ui_size_sort')}
+ aria-hidden="true"
aria-current={sortState ? 'true' : 'false'}
>
{localize('com_ui_size')}
diff --git a/client/src/components/Chat/Input/Files/Table/DataTable.tsx b/client/src/components/Chat/Input/Files/Table/DataTable.tsx
index cb8c3fe3d1..a7cf76a409 100644
--- a/client/src/components/Chat/Input/Files/Table/DataTable.tsx
+++ b/client/src/components/Chat/Input/Files/Table/DataTable.tsx
@@ -1,5 +1,4 @@
import { useState } from 'react';
-import { Search } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import {
flexRender,
@@ -17,7 +16,6 @@ import type {
} from '@tanstack/react-table';
import { FileContext } from 'librechat-data-provider';
import {
- Input,
Table,
Button,
Spinner,
@@ -26,6 +24,7 @@ import {
TableCell,
TableHead,
TrashIcon,
+ FilterInput,
TableHeader,
useMediaQuery,
} from '@librechat/client';
@@ -115,23 +114,13 @@ export default function DataTable({ columns, data }: DataTablePro
)}
{!isSmallScreen && {localize('com_ui_delete')}}
-
-
- table.getColumn('filename')?.setFilterValue(event.target.value)}
- className="peer w-full pl-10 text-sm focus-visible:ring-2 focus-visible:ring-ring"
- aria-label={localize('com_files_filter_input')}
- />
-
-
+ table.getColumn('filename')?.setFilterValue(event.target.value)}
+ containerClassName="flex-1"
+ />
({
{
label: localize('com_ui_ascending'),
onClick: () => column.toggleSorting(false),
- icon: ,
+ icon: ,
},
{
label: localize('com_ui_descending'),
onClick: () => column.toggleSorting(true),
- icon: ,
+ icon: ,
},
];
@@ -56,9 +56,7 @@ export function SortFilterHeader({
items.push({
label: filterValue,
onClick: () => column.setFilterValue(value),
- icon: (
-
- ),
+ icon: ,
show: true,
className: isActive ? 'border-l-2 border-l-border-xheavy' : '',
});
@@ -70,7 +68,7 @@ export function SortFilterHeader({
items.push({
label: localize('com_ui_show_all'),
onClick: () => column.setFilterValue(undefined),
- icon: ,
+ icon: ,
show: true,
});
}
@@ -113,9 +111,9 @@ export function SortFilterHeader({
>
{title}
{column.getIsFiltered() ? (
-
+
) : (
-
+
)}
{(() => {
const sortState = column.getIsSorted();
diff --git a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx
index 36c4ee40e7..d3f0fb65bc 100644
--- a/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx
+++ b/client/src/components/Chat/Input/Files/__tests__/AttachFileMenu.spec.tsx
@@ -278,7 +278,6 @@ describe('AttachFileMenu', () => {
{ name: 'OpenAI', endpoint: EModelEndpoint.openAI },
{ name: 'Anthropic', endpoint: EModelEndpoint.anthropic },
{ name: 'Google', endpoint: EModelEndpoint.google },
- { name: 'Azure OpenAI', endpoint: EModelEndpoint.azureOpenAI },
{ name: 'Custom', endpoint: EModelEndpoint.custom },
];
@@ -301,6 +300,45 @@ describe('AttachFileMenu', () => {
expect(screen.getByText('Upload to Provider')).toBeInTheDocument();
});
});
+
+ it('should show Upload to Provider for Azure OpenAI with useResponsesApi', () => {
+ mockUseAgentToolPermissions.mockReturnValue({
+ fileSearchAllowedByAgent: false,
+ codeAllowedByAgent: false,
+ provider: EModelEndpoint.azureOpenAI,
+ });
+
+ renderAttachFileMenu({
+ endpoint: EModelEndpoint.azureOpenAI,
+ endpointType: EModelEndpoint.azureOpenAI,
+ useResponsesApi: true,
+ });
+
+ const button = screen.getByRole('button', { name: /attach file options/i });
+ fireEvent.click(button);
+
+ expect(screen.getByText('Upload to Provider')).toBeInTheDocument();
+ });
+
+ it('should NOT show Upload to Provider for Azure OpenAI without useResponsesApi', () => {
+ mockUseAgentToolPermissions.mockReturnValue({
+ fileSearchAllowedByAgent: false,
+ codeAllowedByAgent: false,
+ provider: EModelEndpoint.azureOpenAI,
+ });
+
+ renderAttachFileMenu({
+ endpoint: EModelEndpoint.azureOpenAI,
+ endpointType: EModelEndpoint.azureOpenAI,
+ useResponsesApi: false,
+ });
+
+ const button = screen.getByRole('button', { name: /attach file options/i });
+ fireEvent.click(button);
+
+ expect(screen.queryByText('Upload to Provider')).not.toBeInTheDocument();
+ expect(screen.getByText('Upload Image')).toBeInTheDocument();
+ });
});
describe('Agent Capabilities', () => {
@@ -512,7 +550,7 @@ describe('AttachFileMenu', () => {
});
describe('Google Provider Special Case', () => {
- it('should use google_multimodal file type for Google provider', () => {
+ it('should use image_document_video_audio file type for Google provider', () => {
mockUseAgentToolPermissions.mockReturnValue({
fileSearchAllowedByAgent: false,
codeAllowedByAgent: false,
@@ -536,7 +574,7 @@ describe('AttachFileMenu', () => {
// The file input should have been clicked (indirectly tested through the implementation)
});
- it('should use multimodal file type for non-Google providers', () => {
+ it('should use image_document file type for non-Google providers', () => {
mockUseAgentToolPermissions.mockReturnValue({
fileSearchAllowedByAgent: false,
codeAllowedByAgent: false,
@@ -555,7 +593,7 @@ describe('AttachFileMenu', () => {
expect(uploadProviderButton).toBeInTheDocument();
fireEvent.click(uploadProviderButton);
- // Implementation detail - multimodal type is used
+ // Implementation detail - image_document type is used
});
});
diff --git a/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx b/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx
index 44e632fa12..6def1f3d10 100644
--- a/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx
+++ b/client/src/components/Chat/Input/Files/__tests__/DragDropModal.spec.tsx
@@ -63,7 +63,6 @@ describe('DragDropModal - Provider Detection', () => {
{ name: 'OpenAI', value: EModelEndpoint.openAI },
{ name: 'Anthropic', value: EModelEndpoint.anthropic },
{ name: 'Google', value: EModelEndpoint.google },
- { name: 'Azure OpenAI', value: EModelEndpoint.azureOpenAI },
{ name: 'Custom', value: EModelEndpoint.custom },
];
@@ -72,6 +71,10 @@ describe('DragDropModal - Provider Detection', () => {
expect(isDocumentSupportedProvider(value)).toBe(true);
});
});
+
+ it('should NOT recognize Azure OpenAI as supported (requires useResponsesApi)', () => {
+ expect(isDocumentSupportedProvider(EModelEndpoint.azureOpenAI)).toBe(false);
+ });
});
describe('real-world scenarios', () => {
diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx
index 90e22ce17c..278e603db0 100644
--- a/client/src/components/Chat/Input/MCPSelect.tsx
+++ b/client/src/components/Chat/Input/MCPSelect.tsx
@@ -1,8 +1,11 @@
-import React, { memo, useCallback } from 'react';
+import React, { memo, useMemo, useCallback, useRef } from 'react';
+import * as Ariakit from '@ariakit/react';
+import { ChevronDown } from 'lucide-react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
-import { MultiSelect, MCPIcon } from '@librechat/client';
-import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
+import { TooltipAnchor } from '@librechat/client';
+import MCPServerMenuItem from '~/components/MCP/MCPServerMenuItem';
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
+import StackedMCPIcons from '~/components/MCP/StackedMCPIcons';
import { useBadgeRowContext } from '~/Providers';
import { useHasAccess } from '~/hooks';
import { cn } from '~/utils';
@@ -13,96 +16,117 @@ function MCPSelectContent() {
localize,
isPinned,
mcpValues,
- isInitializing,
placeholderText,
- batchToggleServers,
- getConfigDialogProps,
- getServerStatusIconProps,
selectableServers,
+ connectionStatus,
+ isInitializing,
+ getConfigDialogProps,
+ toggleServerSelection,
+ getServerStatusIconProps,
} = mcpServerManager;
- const renderSelectedValues = useCallback(
- (
- values: string[],
- placeholder?: string,
- items?: (string | { label: string; value: string })[],
- ) => {
- if (values.length === 0) {
- return placeholder || localize('com_ui_select_placeholder');
- }
- if (values.length === 1) {
- const selectedItem = items?.find((i) => typeof i !== 'string' && i.value == values[0]);
- return selectedItem && typeof selectedItem !== 'string' ? selectedItem.label : values[0];
- }
- return localize('com_ui_x_selected', { 0: values.length });
+ const menuStore = Ariakit.useMenuStore({ focusLoop: true });
+ const isOpen = menuStore.useState('open');
+ const focusedElementRef = useRef(null);
+
+ const selectedCount = mcpValues?.length ?? 0;
+
+ // Wrap toggleServerSelection to preserve focus after state update
+ const handleToggle = useCallback(
+ (serverName: string) => {
+ // Save currently focused element
+ focusedElementRef.current = document.activeElement as HTMLElement;
+ toggleServerSelection(serverName);
+ // Restore focus after React re-renders
+ requestAnimationFrame(() => {
+ focusedElementRef.current?.focus();
+ });
},
- [localize],
+ [toggleServerSelection],
);
- const renderItemContent = useCallback(
- (serverName: string, defaultContent: React.ReactNode) => {
- const statusIconProps = getServerStatusIconProps(serverName);
- const isServerInitializing = isInitializing(serverName);
+ const selectedServers = useMemo(() => {
+ if (!mcpValues || mcpValues.length === 0) {
+ return [];
+ }
+ return selectableServers.filter((s) => mcpValues.includes(s.serverName));
+ }, [selectableServers, mcpValues]);
- /**
- Common wrapper for the main content (check mark + text).
- Ensures Check & Text are adjacent and the group takes available space.
- */
- const mainContentWrapper = (
-
- );
-
- const statusIcon = statusIconProps && ;
-
- if (statusIcon) {
- return (
-
- {mainContentWrapper}
-
{statusIcon}
-
- );
- }
-
- return mainContentWrapper;
- },
- [getServerStatusIconProps, isInitializing],
- );
+ const displayText = useMemo(() => {
+ if (selectedCount === 0) {
+ return null;
+ }
+ if (selectedCount === 1) {
+ const server = selectableServers.find((s) => s.serverName === mcpValues?.[0]);
+ return server?.config?.title || mcpValues?.[0];
+ }
+ return localize('com_ui_x_selected', { 0: selectedCount });
+ }, [selectedCount, selectableServers, mcpValues, localize]);
if (!isPinned && mcpValues?.length === 0) {
return null;
}
const configDialogProps = getConfigDialogProps();
+
return (
<>
- ({
- label: s.config.title || s.serverName,
- value: s.serverName,
- }))}
- selectedValues={mcpValues ?? []}
- setSelectedValues={batchToggleServers}
- renderSelectedValues={renderSelectedValues}
- renderItemContent={renderItemContent}
- placeholder={placeholderText}
- popoverClassName="min-w-fit"
- className="badge-icon min-w-fit"
- selectIcon={}
- selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
- selectClassName={cn(
- 'group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all',
- 'md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner',
- )}
- />
+
+
+ }
+ >
+
+
+ {displayText || placeholderText}
+
+
+
+
+
+
+ {selectableServers.map((server) => (
+
+ ))}
+
+
+
{configDialogProps && (
)}
diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx
index 38e6167b65..66b816e934 100644
--- a/client/src/components/Chat/Input/MCPSubMenu.tsx
+++ b/client/src/components/Chat/Input/MCPSubMenu.tsx
@@ -1,10 +1,11 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronRight } from 'lucide-react';
-import { PinIcon, MCPIcon } from '@librechat/client';
-import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon';
+import { MCPIcon, PinIcon } from '@librechat/client';
+import MCPServerMenuItem from '~/components/MCP/MCPServerMenuItem';
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
import { useBadgeRowContext } from '~/Providers';
+import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface MCPSubMenuProps {
@@ -13,14 +14,16 @@ interface MCPSubMenuProps {
const MCPSubMenu = React.forwardRef(
({ placeholder, ...props }, ref) => {
+ const localize = useLocalize();
const { mcpServerManager } = useBadgeRowContext();
const {
isPinned,
mcpValues,
setIsPinned,
- isInitializing,
placeholderText,
- availableMCPServers,
+ selectableServers,
+ connectionStatus,
+ isInitializing,
getConfigDialogProps,
toggleServerSelection,
getServerStatusIconProps,
@@ -33,7 +36,7 @@ const MCPSubMenu = React.forwardRef(
});
// Don't render if no MCP servers are configured
- if (!availableMCPServers || availableMCPServers.length === 0) {
+ if (!selectableServers || selectableServers.length === 0) {
return null;
}
@@ -44,6 +47,7 @@ const MCPSubMenu = React.forwardRef(
) => {
@@ -55,9 +59,9 @@ const MCPSubMenu = React.forwardRef(
}
>
-
+
{placeholder || placeholderText}
-
+
+
- {availableMCPServers.map((s) => {
- const statusIconProps = getServerStatusIconProps(s.serverName);
- const isSelected = mcpValues?.includes(s.serverName) ?? false;
- const isServerInitializing = isInitializing(s.serverName);
-
- const statusIcon = statusIconProps && ;
-
- return (
- {
- event.preventDefault();
- toggleServerSelection(s.serverName);
- }}
- disabled={isServerInitializing}
- className={cn(
- 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer',
- 'scroll-m-1 outline-none transition-colors',
- 'hover:bg-black/[0.075] dark:hover:bg-white/10',
- 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
- 'w-full min-w-0 justify-between text-sm',
- isServerInitializing &&
- 'opacity-50 hover:bg-transparent dark:hover:bg-transparent',
- isSelected && 'bg-surface-active',
- )}
- >
-
-
-
{s.config.title || s.serverName}
-
- {statusIcon && {statusIcon}
}
-
- );
- })}
+
+ {selectableServers.map((server) => (
+
+ ))}
+
{configDialogProps && }
diff --git a/client/src/components/Chat/Input/StreamAudio.tsx b/client/src/components/Chat/Input/StreamAudio.tsx
index 83eb9e7fae..221acaaa2c 100644
--- a/client/src/components/Chat/Input/StreamAudio.tsx
+++ b/client/src/components/Chat/Input/StreamAudio.tsx
@@ -39,7 +39,7 @@ export default function StreamAudio({ index = 0 }) {
const { pauseGlobalAudio } = usePauseGlobalAudio();
const { conversationId: paramId } = useParams();
- const queryParam = paramId === 'new' ? paramId : latestMessage?.conversationId ?? paramId ?? '';
+ const queryParam = paramId === 'new' ? paramId : (latestMessage?.conversationId ?? paramId ?? '');
const queryClient = useQueryClient();
const getMessages = useCallback(
diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx
index af464dfe39..98d35914c5 100644
--- a/client/src/components/Chat/Input/ToolsDropdown.tsx
+++ b/client/src/components/Chat/Input/ToolsDropdown.tsx
@@ -311,7 +311,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
id="tools-dropdown-button"
aria-label="Tools Options"
className={cn(
- 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
+ 'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
isPopoverActive && 'bg-surface-hover',
)}
>
diff --git a/client/src/components/Chat/Menus/BookmarkMenu.tsx b/client/src/components/Chat/Menus/BookmarkMenu.tsx
index f8ee05b2a4..3dff6611e5 100644
--- a/client/src/components/Chat/Menus/BookmarkMenu.tsx
+++ b/client/src/components/Chat/Menus/BookmarkMenu.tsx
@@ -147,9 +147,9 @@ const BookmarkMenu: FC = () => {
return ;
}
if ((tags?.length ?? 0) > 0) {
- return ;
+ return ;
}
- return ;
+ return ;
};
return (
diff --git a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx
index 70a5b0b5e0..2ce578b8e4 100644
--- a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx
+++ b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx
@@ -48,8 +48,8 @@ export const CustomMenu = React.forwardRef(func
!parent &&
'flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-border-light px-3 py-2 text-sm text-text-primary',
menuStore.useState('open')
- ? 'bg-surface-tertiary hover:bg-surface-tertiary'
- : 'bg-surface-secondary hover:bg-surface-tertiary',
+ ? 'bg-surface-active-alt hover:bg-surface-active-alt'
+ : 'bg-presentation hover:bg-surface-active-alt',
props.className,
)}
render={parent ? : trigger}
@@ -66,7 +66,7 @@ export const CustomMenu = React.forwardRef(func
className={cn(
`${parent ? 'animate-popover-left ml-3' : 'animate-popover'} outline-none! z-50 flex max-h-[min(450px,var(--popover-available-height))] w-full`,
'w-[var(--menu-width,auto)] min-w-[300px] flex-col overflow-auto rounded-xl border border-border-light',
- 'bg-surface-secondary px-3 py-2 text-sm text-text-primary shadow-lg',
+ 'bg-presentation px-3 py-2 text-sm text-text-primary shadow-lg',
'max-w-[calc(100vw-4rem)] sm:max-h-[calc(65vh)] sm:max-w-[400px]',
searchable && 'p-0',
)}
@@ -80,13 +80,13 @@ export const CustomMenu = React.forwardRef(func
autoSelect
render={combobox}
className={cn(
- 'peer mt-1 h-10 w-full rounded-lg border-none bg-transparent px-2 text-base',
+ 'peer flex h-10 w-full items-center justify-center rounded-lg border-none bg-transparent px-2 text-base',
'sm:h-8 sm:text-sm',
- 'focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-white',
+ 'focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-primary',
)}
/>
{comboboxLabel && (
-