feat: wire MCPAppRenderer into ChatPanel for MCP App tool results

This commit is contained in:
Ken
2026-05-26 19:39:34 +00:00
parent 12d1df417f
commit 9d118d60ce
2 changed files with 87 additions and 3 deletions
+13 -2
View File
@@ -3,6 +3,7 @@ import type { KeyboardEvent } from 'react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import { IconSend } from '@tabler/icons-react' import { IconSend } from '@tabler/icons-react'
import { ToolCallCard } from './ToolCallCard' import { ToolCallCard } from './ToolCallCard'
import { MCPAppRenderer } from './MCPAppRenderer'
import type { ChatMessage } from '../hooks/useChat' import type { ChatMessage } from '../hooks/useChat'
interface ChatPanelProps { interface ChatPanelProps {
@@ -61,9 +62,19 @@ export function ChatPanel({ messages, isStreaming, onSend, sessionId }: ChatPane
{message.role === 'assistant' ? ( {message.role === 'assistant' ? (
<div> <div>
<ReactMarkdown>{message.content}</ReactMarkdown> <ReactMarkdown>{message.content}</ReactMarkdown>
{message.toolCalls?.map(tc => ( {message.toolCalls?.map(tc =>
tc._meta?.ui?.resourceUri ? (
<MCPAppRenderer
key={tc.id}
resourceUri={tc._meta.ui.resourceUri}
data={tc.structuredContent ?? {}}
csp={tc._meta.ui.csp}
onSendMessage={(content) => onSend(content)}
/>
) : (
<ToolCallCard key={tc.id} toolCall={tc} /> <ToolCallCard key={tc.id} toolCall={tc} />
))} )
)}
</div> </div>
) : ( ) : (
<div>{message.content}</div> <div>{message.content}</div>
+73
View File
@@ -14,6 +14,13 @@ vi.mock('react-markdown', () => ({
default: ({ children }: { children: string }) => <div>{children}</div>, default: ({ children }: { children: string }) => <div>{children}</div>,
})) }))
// Mock MCPAppRenderer to avoid fetch/iframe in test environment
vi.mock('../components/MCPAppRenderer', () => ({
MCPAppRenderer: ({ resourceUri }: { resourceUri: string }) => (
<div data-testid='mcp-app-renderer' data-resource-uri={resourceUri} />
),
}))
describe('ChatPanel', () => { describe('ChatPanel', () => {
const noop = () => undefined const noop = () => undefined
@@ -90,6 +97,72 @@ describe('ChatPanel', () => {
expect(container.querySelector('.tool-call-card')).toBeTruthy() expect(container.querySelector('.tool-call-card')).toBeTruthy()
}) })
it('renders MCPAppRenderer for tool calls with _meta.ui.resourceUri', () => {
const messages: ChatMessage[] = [
{
id: '1',
role: 'assistant',
content: 'Running an MCP app',
toolCalls: [
{
id: 'tc-mcp',
name: 'mcp_app',
args: {},
status: 'complete',
_meta: { ui: { resourceUri: 'app://chart' } },
structuredContent: { value: 42 },
},
],
},
]
render(
<ChatPanel messages={messages} isStreaming={false} onSend={noop} sessionId={sessionId} />
)
expect(screen.getByTestId('mcp-app-renderer')).toBeTruthy()
})
it('does not render ToolCallCard for tool calls with _meta.ui.resourceUri', () => {
const messages: ChatMessage[] = [
{
id: '1',
role: 'assistant',
content: 'Running an MCP app',
toolCalls: [
{
id: 'tc-mcp',
name: 'mcp_app',
args: {},
status: 'complete',
_meta: { ui: { resourceUri: 'app://chart' } },
structuredContent: { value: 42 },
},
],
},
]
const { container } = render(
<ChatPanel messages={messages} isStreaming={false} onSend={noop} sessionId={sessionId} />
)
expect(container.querySelector('.tool-call-card')).toBeNull()
})
it('renders ToolCallCard for tool calls without _meta.ui.resourceUri', () => {
const messages: ChatMessage[] = [
{
id: '1',
role: 'assistant',
content: 'Running a plain tool',
toolCalls: [
{ id: 'tc-plain', name: 'bash', args: {}, status: 'complete' },
],
},
]
const { container } = render(
<ChatPanel messages={messages} isStreaming={false} onSend={noop} sessionId={sessionId} />
)
expect(container.querySelector('.tool-call-card')).toBeTruthy()
expect(screen.queryByTestId('mcp-app-renderer')).toBeNull()
})
it('shows Researching... when isStreaming and messages exist', () => { it('shows Researching... when isStreaming and messages exist', () => {
const messages: ChatMessage[] = [ const messages: ChatMessage[] = [
{ id: '1', role: 'user', content: 'Question' }, { id: '1', role: 'user', content: 'Question' },