mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: thread configured server names through display parsing
parseToolName and getMCPServerName resolved context-free, so a configured server whose name contains the delimiter showed the wrong server in grouped tool summaries and subagent tool labels, and stacked icons missed its entry in the icon map. Both take the configured names now, supplied by the components that render them. Adds the hook to SubagentCall's mock factory: the spec renders the real component, so an unmocked useMCPServerNames would reach the query with no provider.
This commit is contained in:
parent
07404c0cf9
commit
c47e5b706b
6 changed files with 34 additions and 14 deletions
|
|
@ -16,6 +16,7 @@ import { MessageContext } from '~/Providers/MessageContext';
|
|||
import MessageIcon from '~/components/Share/MessageIcon';
|
||||
import { subagentProgressByToolCallId } from '~/store';
|
||||
import { useAgentsMapContext } from '~/Providers';
|
||||
import { useMCPServerNames } from '~/hooks/MCP';
|
||||
import { AttachmentGroup } from './Attachment';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Reasoning from './Reasoning';
|
||||
|
|
@ -704,11 +705,13 @@ function ToolNameBadge({ name }: { name: string }): JSX.Element {
|
|||
function ToolIdentifier({
|
||||
rawName,
|
||||
localize,
|
||||
mcpServerNames,
|
||||
}: {
|
||||
rawName: string;
|
||||
localize: ReturnType<typeof useLocalize>;
|
||||
mcpServerNames?: readonly string[];
|
||||
}): JSX.Element {
|
||||
const parsed = parseToolName(rawName);
|
||||
const parsed = parseToolName(rawName, mcpServerNames);
|
||||
if (parsed.mcpServer) {
|
||||
return (
|
||||
<span className="inline-flex min-w-0 shrink items-baseline gap-1">
|
||||
|
|
@ -740,6 +743,7 @@ function ToolIdentifier({
|
|||
*/
|
||||
function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
|
||||
const localize = useLocalize();
|
||||
const mcpServerNames = useMCPServerNames();
|
||||
if (line.kind === 'writing' || line.kind === 'reasoning') {
|
||||
const prefix =
|
||||
line.kind === 'writing'
|
||||
|
|
@ -766,7 +770,7 @@ function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
|
|||
{line.toolNames.map((name, i) => (
|
||||
<span key={`${i}-${name}`} className="flex min-w-0 items-baseline gap-1">
|
||||
{i > 0 && <span className="shrink-0 text-text-tertiary">,</span>}
|
||||
<ToolIdentifier rawName={name} localize={localize} />
|
||||
<ToolIdentifier rawName={name} localize={localize} mcpServerNames={mcpServerNames} />
|
||||
</span>
|
||||
))}
|
||||
{line.argsSnippet && (
|
||||
|
|
@ -779,7 +783,11 @@ function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
|
|||
if (line.kind === 'tool_complete') {
|
||||
return (
|
||||
<li className="flex w-full items-baseline gap-1 overflow-hidden whitespace-nowrap">
|
||||
<ToolIdentifier rawName={line.toolName} localize={localize} />
|
||||
<ToolIdentifier
|
||||
rawName={line.toolName}
|
||||
localize={localize}
|
||||
mcpServerNames={mcpServerNames}
|
||||
/>
|
||||
<span className="shrink-0 text-text-tertiary">→</span>
|
||||
<span
|
||||
dir="rtl"
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ jest.mock('~/components/Share/MessageIcon', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/MCP', () => {
|
||||
const mcpServerNames: string[] = [];
|
||||
return { useMCPServerNames: () => mcpServerNames };
|
||||
});
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
...jest.requireActual('~/utils/groupToolCalls'),
|
||||
...jest.requireActual('~/utils/toolLabels'),
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import type {
|
|||
} from 'librechat-data-provider';
|
||||
import type { PartWithIndex } from './ParallelContent';
|
||||
import { useLocalize, useExpandCollapse, scheduleMessageContentLayoutReconcile } from '~/hooks';
|
||||
import { useMCPIconMap, useMCPServerNames } from '~/hooks/MCP';
|
||||
import { isBashProgrammaticToolCall } from './routing';
|
||||
import { ASK_USER_QUESTION } from '~/utils/approval';
|
||||
import { cn, getToolDisplayLabel } from '~/utils';
|
||||
import { StackedToolIcons } from './ToolOutput';
|
||||
import { useMCPIconMap } from '~/hooks/MCP';
|
||||
import { AttachmentGroup } from './Parts';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -126,6 +126,7 @@ export default function ToolCallGroup({
|
|||
}: ToolCallGroupProps) {
|
||||
const localize = useLocalize();
|
||||
const mcpIconMap = useMCPIconMap();
|
||||
const mcpServerNames = useMCPServerNames();
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const cancelLayoutReconcileRef = useRef<(() => void) | null>(null);
|
||||
const retainedForPendingApprovalRef = useRef(false);
|
||||
|
|
@ -179,7 +180,7 @@ export default function ToolCallGroup({
|
|||
const labels: string[] = [];
|
||||
for (const rawName of toolNames) {
|
||||
if (!rawName) continue;
|
||||
const label = getToolDisplayLabel(rawName, localize);
|
||||
const label = getToolDisplayLabel(rawName, localize, mcpServerNames);
|
||||
if (!seen.has(label)) {
|
||||
seen.add(label);
|
||||
labels.push(label);
|
||||
|
|
@ -189,7 +190,7 @@ export default function ToolCallGroup({
|
|||
return labels.join(', ');
|
||||
}
|
||||
return `${labels.slice(0, 3).join(', ')}, +${labels.length - 3}`;
|
||||
}, [toolNames, localize]);
|
||||
}, [toolNames, localize, mcpServerNames]);
|
||||
|
||||
const autoExpand = useRecoilValue(store.autoExpandTools);
|
||||
const autoCollapse = !autoExpand && count >= 2 && allCompleted;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import ToolIcon, { getToolIconType, getMCPServerName } from './ToolIcon';
|
||||
import type { ToolIconType } from './ToolIcon';
|
||||
import ToolIcon, { getToolIconType, getMCPServerName } from './ToolIcon';
|
||||
import { useMCPServerNames } from '~/hooks/MCP';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface ResolvedIcon {
|
||||
|
|
@ -22,12 +23,13 @@ export default function StackedToolIcons({
|
|||
maxIcons = 3,
|
||||
isAnimating = false,
|
||||
}: StackedToolIconsProps) {
|
||||
const mcpServerNames = useMCPServerNames();
|
||||
const uniqueIcons = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
const result: ResolvedIcon[] = [];
|
||||
for (const name of toolNames) {
|
||||
const type = getToolIconType(name);
|
||||
const serverName = getMCPServerName(name);
|
||||
const serverName = getMCPServerName(name, mcpServerNames);
|
||||
const iconUrl = serverName ? mcpIconMap?.get(serverName) : undefined;
|
||||
const key = iconUrl ? `mcp-${serverName}` : type;
|
||||
if (!seen.has(key)) {
|
||||
|
|
@ -36,7 +38,7 @@ export default function StackedToolIcons({
|
|||
}
|
||||
}
|
||||
return result;
|
||||
}, [toolNames, mcpIconMap]);
|
||||
}, [toolNames, mcpIconMap, mcpServerNames]);
|
||||
|
||||
const visibleIcons = uniqueIcons.slice(0, maxIcons);
|
||||
const overflowCount = uniqueIcons.length - visibleIcons.length;
|
||||
|
|
|
|||
|
|
@ -91,11 +91,11 @@ export function getToolIconType(name: string): ToolIconType {
|
|||
}
|
||||
|
||||
/** Extracts the MCP server name from a tool name with format `tool<delimiter>server`. */
|
||||
export function getMCPServerName(toolName: string): string {
|
||||
export function getMCPServerName(toolName: string, knownServerNames?: readonly string[]): string {
|
||||
if (!toolName.includes(Constants.mcp_delimiter)) {
|
||||
return '';
|
||||
}
|
||||
const [, serverName] = splitToolCallName(toolName);
|
||||
const [, serverName] = splitToolCallName(toolName, knownServerNames);
|
||||
return serverName ?? '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,12 @@ export interface ParsedToolName {
|
|||
* - `web_search` → `{ mcpServer: '', toolName: 'web_search', friendlyKey: 'com_ui_tool_name_web_search' }`
|
||||
* - `some_custom_tool` → `{ mcpServer: '', toolName: 'some_custom_tool' }`
|
||||
*/
|
||||
export function parseToolName(rawName: string): ParsedToolName {
|
||||
export function parseToolName(
|
||||
rawName: string,
|
||||
knownServerNames?: readonly string[],
|
||||
): ParsedToolName {
|
||||
if (rawName.includes(Constants.mcp_delimiter)) {
|
||||
const [toolName, mcpServer = ''] = splitToolCallName(rawName);
|
||||
const [toolName, mcpServer = ''] = splitToolCallName(rawName, knownServerNames);
|
||||
return { raw: rawName, mcpServer, toolName };
|
||||
}
|
||||
const friendlyKey = TOOL_FRIENDLY_NAME_KEYS[rawName];
|
||||
|
|
@ -72,8 +75,9 @@ export function parseToolName(rawName: string): ParsedToolName {
|
|||
export function getToolDisplayLabel(
|
||||
rawName: string,
|
||||
localize: (key: TranslationKeys) => string,
|
||||
knownServerNames?: readonly string[],
|
||||
): string {
|
||||
const parsed = parseToolName(rawName);
|
||||
const parsed = parseToolName(rawName, knownServerNames);
|
||||
if (parsed.mcpServer) return parsed.mcpServer;
|
||||
if (parsed.friendlyKey) return localize(parsed.friendlyKey);
|
||||
return parsed.toolName;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue