Clean room implementation of the chat process (#5079)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Zane <75694352+zanesq@users.noreply.github.com>
This commit is contained in:
Douwe Osinga
2025-10-11 10:45:16 -04:00
committed by GitHub
parent 73f109237e
commit 0c2127124f
36 changed files with 1024 additions and 541 deletions
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Button } from './ui/button';
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent';
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
import { cn, snakeToTitleCase } from '../utils';
import { LoadingStatus } from './ui/Dot';
import { NotificationEvent } from '../hooks/useMessageStream';
@@ -12,6 +12,7 @@ import { ChevronRight, FlaskConical } from 'lucide-react';
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
import { isUIResource } from '@mcp-ui/client';
import { Content, EmbeddedResource } from '../api';
interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
@@ -22,16 +23,34 @@ interface ToolCallWithResponseProps {
append?: (value: string) => void; // Function to append messages to the chat
}
function getToolResultValue(toolResult: Record<string, unknown>): Content[] | null {
if ('value' in toolResult && Array.isArray(toolResult.value)) {
return toolResult.value as Content[];
}
return null;
}
function isEmbeddedResource(content: Content): content is EmbeddedResource {
return 'resource' in content && typeof (content as Record<string, unknown>).resource === 'object';
}
export default function ToolCallWithResponse({
isCancelledMessage,
toolRequest,
toolResponse,
notifications,
isStreamingMessage = false,
isStreamingMessage,
append,
}: ToolCallWithResponseProps) {
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
if (!toolCall) {
// Handle both the wrapped ToolResult format and the unwrapped format
// The server serializes ToolResult<T> as { status: "success", value: T } or { status: "error", error: string }
const toolCallData = toolRequest.toolCall as Record<string, unknown>;
const toolCall =
toolCallData?.status === 'success'
? (toolCallData.value as { name: string; arguments: Record<string, unknown> })
: (toolCallData as { name: string; arguments: Record<string, unknown> });
if (!toolCall || !toolCall.name) {
return null;
}
@@ -53,12 +72,15 @@ export default function ToolCallWithResponse({
/>
</div>
{/* MCP UI — Inline */}
{toolResponse?.toolResult?.value &&
toolResponse.toolResult.value.map((content, index) => {
if (isUIResource(content)) {
{toolResponse?.toolResult &&
getToolResultValue(toolResponse.toolResult)?.map((content, index) => {
const resourceContent = isEmbeddedResource(content)
? { ...content, type: 'resource' as const }
: null;
if (resourceContent && isUIResource(resourceContent)) {
return (
<div key={`${content.type}-${index}`} className="mt-3">
<MCPUIResourceRenderer content={content} appendPromptToChat={append} />
<div key={index} className="mt-3">
<MCPUIResourceRenderer content={resourceContent} appendPromptToChat={append} />
<div className="mt-3 p-4 py-3 border border-borderSubtle rounded-lg bg-background-muted flex items-center">
<FlaskConical className="mr-2" size={20} />
<div className="text-sm font-sans">
@@ -200,7 +222,7 @@ function ToolCallView({
}
})();
const isToolDetails = Object.entries(toolCall?.arguments).length > 0;
const isToolDetails = toolCall?.arguments && Object.entries(toolCall.arguments).length > 0;
// Check if streaming has finished but no tool response was received
// This is a workaround for cases where the backend doesn't send tool responses
@@ -211,7 +233,9 @@ function ToolCallView({
? shouldShowAsComplete
? 'success'
: 'loading'
: toolResponse.toolResult.status;
: (toolResponse.toolResult as Record<string, unknown>).status === 'error'
? 'error'
: 'success';
// Tool call timing tracking
const [startTime, setStartTime] = useState<number | null>(null);
@@ -547,19 +571,30 @@ interface ToolResultViewProps {
}
function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
const hasText = (c: Content): c is Content & { text: string } =>
'text' in c && typeof (c as Record<string, unknown>).text === 'string';
const hasImage = (c: Content): c is Content & { data: string; mimeType: string } => {
if (!('data' in c && 'mimeType' in c)) return false;
const mimeType = (c as Record<string, unknown>).mimeType;
return typeof mimeType === 'string' && mimeType.startsWith('image');
};
const hasResource = (c: Content): c is Content & { resource: unknown } => 'resource' in c;
return (
<ToolCallExpandable
label={<span className="pl-4 py-1 font-sans text-sm">Output</span>}
isStartExpanded={isStartExpanded}
>
<div className="pl-4 pr-4 py-4">
{result.type === 'text' && result.text && (
{hasText(result) && (
<MarkdownContent
content={result.text}
className="whitespace-pre-wrap max-w-full overflow-x-auto"
/>
)}
{result.type === 'image' && (
{hasImage(result) && (
<img
src={`data:${result.mimeType};base64,${result.data}`}
alt="Tool result"
@@ -570,7 +605,7 @@ function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
}}
/>
)}
{result.type === 'resource' && (
{hasResource(result) && (
<pre className="font-sans text-sm">{JSON.stringify(result, null, 2)}</pre>
)}
</div>