feat: wire MCPAppRenderer into ChatPanel for MCP App tool results
This commit is contained in:
@@ -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 =>
|
||||||
<ToolCallCard key={tc.id} toolCall={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} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>{message.content}</div>
|
<div>{message.content}</div>
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user