mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: upgrade @mcp-ui/client package and improve UI message handling (#4164)
This commit is contained in:
Generated
+9
-41
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.14",
|
||||
"@ai-sdk/ui-utils": "^1.2.11",
|
||||
"@mcp-ui/client": "~5.6.2",
|
||||
"@mcp-ui/client": "^5.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -2601,18 +2601,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.6.2.tgz",
|
||||
"integrity": "sha512-CLHin0eDM+0m0AmSc/PS0XgZAU2D+b/ppt4CxLykTck4CrQ0Gi589ieqh9VidijiB9R5Aw3F8Wi/IUr6Tp6urg==",
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.9.0.tgz",
|
||||
"integrity": "sha512-I7ZAZKSo08GtDRrPZNlD8Ij6EFOJvnIfsal6MIwYxY8AtMkbLkI+DDkM9QhSmMDDJvK0jtqWEVu+5KZK+pQYlQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "*",
|
||||
"@quilted/threads": "^3.1.3",
|
||||
"@r2wc/react-to-web-component": "^2.0.4",
|
||||
"@remote-dom/core": "^1.8.0",
|
||||
"@remote-dom/react": "^1.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"@remote-dom/react": "^1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/@remote-dom/react": {
|
||||
@@ -2647,40 +2649,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mcp-ui/client/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^2.0.14",
|
||||
"@ai-sdk/ui-utils": "^1.2.11",
|
||||
"@mcp-ui/client": "~5.6.2",
|
||||
"@mcp-ui/client": "^5.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -140,6 +140,10 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "Apache-2.0",
|
||||
"overrides": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"bash -c 'npm run typecheck'",
|
||||
|
||||
@@ -332,6 +332,21 @@ function BaseChatContent({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Listen for global scroll-to-bottom requests (e.g., from MCP UI prompt actions)
|
||||
useEffect(() => {
|
||||
const handleGlobalScrollRequest = () => {
|
||||
// Add a small delay to ensure content has been rendered
|
||||
setTimeout(() => {
|
||||
if (scrollRef.current?.scrollToBottom) {
|
||||
scrollRef.current.scrollToBottom();
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll-chat-to-bottom', handleGlobalScrollRequest);
|
||||
return () => window.removeEventListener('scroll-chat-to-bottom', handleGlobalScrollRequest);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<MainPanelLayout
|
||||
|
||||
@@ -208,6 +208,7 @@ export default function GooseMessage({
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
notifications={toolCallNotifications.get(toolRequest.id)}
|
||||
isStreamingMessage={isStreaming}
|
||||
append={append}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,74 +1,416 @@
|
||||
import { UIResourceRenderer, UIActionResult } from '@mcp-ui/client';
|
||||
import {
|
||||
UIResourceRenderer,
|
||||
UIActionResultIntent,
|
||||
UIActionResultLink,
|
||||
UIActionResultNotification,
|
||||
UIActionResultPrompt,
|
||||
UIActionResultToolCall,
|
||||
} from '@mcp-ui/client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ResourceContent } from '../types/message';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface MCPUIResourceRendererProps {
|
||||
content: ResourceContent;
|
||||
appendPromptToChat?: (value: string) => void;
|
||||
}
|
||||
type UISizeChange = {
|
||||
type: 'ui-size-change';
|
||||
payload: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
|
||||
// Reserved message types from iframe to host
|
||||
type UILifecycleIframeReady = {
|
||||
type: 'ui-lifecycle-iframe-ready';
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type UIRequestData = {
|
||||
type: 'ui-request-data';
|
||||
messageId: string;
|
||||
payload: {
|
||||
requestType: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
// We are creating a new type to support all reserved message types that may come from the iframe
|
||||
// Not all reserved message types are currently exported by @mcp-ui/client
|
||||
type ActionEventsFromIframe =
|
||||
| UIActionResultIntent
|
||||
| UIActionResultLink
|
||||
| UIActionResultNotification
|
||||
| UIActionResultPrompt
|
||||
| UIActionResultToolCall
|
||||
| UISizeChange
|
||||
| UILifecycleIframeReady
|
||||
| UIRequestData;
|
||||
|
||||
// More specific result types using discriminated unions
|
||||
type UIActionHandlerSuccess<T = unknown> = {
|
||||
status: 'success';
|
||||
data?: T;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type UIActionHandlerError = {
|
||||
status: 'error';
|
||||
error: {
|
||||
code: UIActionErrorCode;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type UIActionHandlerPending = {
|
||||
status: 'pending';
|
||||
message: string;
|
||||
};
|
||||
|
||||
type UIActionHandlerResult<T = unknown> =
|
||||
| UIActionHandlerSuccess<T>
|
||||
| UIActionHandlerError
|
||||
| UIActionHandlerPending;
|
||||
|
||||
// Strongly typed error codes
|
||||
enum UIActionErrorCode {
|
||||
UNSUPPORTED_ACTION = 'UNSUPPORTED_ACTION',
|
||||
UNKNOWN_ACTION = 'UNKNOWN_ACTION',
|
||||
TOOL_NOT_FOUND = 'TOOL_NOT_FOUND',
|
||||
TOOL_EXECUTION_FAILED = 'TOOL_EXECUTION_FAILED',
|
||||
NAVIGATION_FAILED = 'NAVIGATION_FAILED',
|
||||
PROMPT_FAILED = 'PROMPT_FAILED',
|
||||
INTENT_FAILED = 'INTENT_FAILED',
|
||||
INVALID_PARAMS = 'INVALID_PARAMS',
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
}
|
||||
|
||||
export default function MCPUIResourceRenderer({ content }: MCPUIResourceRendererProps) {
|
||||
const handleAction = (action: UIActionResult) => {
|
||||
console.log(
|
||||
`MCP UI message received (but only handled with a toast notification for now):`,
|
||||
action
|
||||
);
|
||||
toast.info(`${action.type} message sent from MCP UI, refer to console for more info`, {
|
||||
data: action,
|
||||
});
|
||||
return { status: 'handled', message: `${action.type} action logged` };
|
||||
};
|
||||
// toast component
|
||||
const ToastComponent = ({
|
||||
messageType,
|
||||
message,
|
||||
isImplemented = true,
|
||||
}: {
|
||||
messageType: string;
|
||||
message?: string;
|
||||
isImplemented?: boolean;
|
||||
}) => {
|
||||
const title = `MCP-UI ${messageType} message`;
|
||||
|
||||
const handleUIAction = useCallback(async (result: UIActionResult) => {
|
||||
switch (result.type) {
|
||||
case 'intent': {
|
||||
// TODO: Implement intent handling
|
||||
handleAction(result);
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-0 py-2 pr-4">
|
||||
<p className="font-bold">{title}</p>
|
||||
{isImplemented ? (
|
||||
<p>
|
||||
Message received for <span className="font-bold">{message}</span>.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Message received for <span className="font-bold">{message}</span>.
|
||||
<br />
|
||||
{messageType.charAt(0).toUpperCase() + messageType.slice(1)} messages aren't supported
|
||||
yet, refer to console for more details.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
case 'link': {
|
||||
// TODO: Implement link handling
|
||||
handleAction(result);
|
||||
break;
|
||||
}
|
||||
export default function MCPUIResourceRenderer({
|
||||
content,
|
||||
appendPromptToChat,
|
||||
}: MCPUIResourceRendererProps) {
|
||||
const [currentThemeValue, setCurrentThemeValue] = useState<string>('light');
|
||||
|
||||
case 'notify': {
|
||||
// TODO: Implement notify handling
|
||||
handleAction(result);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'prompt': {
|
||||
// TODO: Implement prompt handling
|
||||
handleAction(result);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool': {
|
||||
// TODO: Implement tool call handling
|
||||
handleAction(result);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.warn('unsupported message sent from MCP-UI:', result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
setCurrentThemeValue(theme);
|
||||
console.log('[MCP-UI] Current theme value:', theme);
|
||||
}, []);
|
||||
|
||||
const handleUIAction = async (
|
||||
actionEvent: ActionEventsFromIframe
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
// result to pass back to the MCP-UI
|
||||
let result: UIActionHandlerResult;
|
||||
|
||||
const handleToolCase = async (
|
||||
actionEvent: UIActionResultToolCall
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
const { toolName, params } = actionEvent.payload;
|
||||
toast.info(<ToastComponent messageType="tool" message={toolName} isImplemented={false} />, {
|
||||
theme: currentThemeValue,
|
||||
});
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.UNSUPPORTED_ACTION,
|
||||
message: 'Tool calls are not yet implemented',
|
||||
details: { toolName, params },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handlePromptCase = async (
|
||||
actionEvent: UIActionResultPrompt
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
const { prompt } = actionEvent.payload;
|
||||
|
||||
if (appendPromptToChat) {
|
||||
try {
|
||||
appendPromptToChat(prompt);
|
||||
window.dispatchEvent(new CustomEvent('scroll-chat-to-bottom'));
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Prompt sent to chat successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.PROMPT_FAILED,
|
||||
message: 'Failed to send prompt to chat',
|
||||
details: error instanceof Error ? error.message : error,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.UNSUPPORTED_ACTION,
|
||||
message: 'Prompt handling is not implemented - append prop is required',
|
||||
details: { prompt },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleLinkCase = async (actionEvent: UIActionResultLink) => {
|
||||
const { url } = actionEvent.payload;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.NAVIGATION_FAILED,
|
||||
message: `Blocked potentially unsafe URL protocol: ${urlObj.protocol}`,
|
||||
details: { url, protocol: urlObj.protocol },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await window.electron.openExternal(url);
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: `Opened ${url} in default browser`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError && error.message.includes('Invalid URL')) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.INVALID_PARAMS,
|
||||
message: `Invalid URL format: ${url}`,
|
||||
details: { url, error: error.message },
|
||||
},
|
||||
};
|
||||
} else if (error instanceof Error && error.message.includes('Failed to open')) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.NAVIGATION_FAILED,
|
||||
message: `Failed to open URL in default browser`,
|
||||
details: { url, error: error.message },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.NAVIGATION_FAILED,
|
||||
message: `Unexpected error opening URL: ${url}`,
|
||||
details: error instanceof Error ? error.message : error,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifyCase = async (
|
||||
actionEvent: UIActionResultNotification
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
const { message } = actionEvent.payload;
|
||||
|
||||
toast.info(<ToastComponent messageType="notify" message={message} isImplemented={true} />, {
|
||||
theme: currentThemeValue,
|
||||
});
|
||||
return {
|
||||
status: 'success' as const,
|
||||
data: {
|
||||
displayedAt: new Date().toISOString(),
|
||||
message: 'Notification displayed',
|
||||
details: actionEvent.payload,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleIntentCase = async (
|
||||
actionEvent: UIActionResultIntent
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
toast.info(
|
||||
<ToastComponent
|
||||
messageType="intent"
|
||||
message={actionEvent.payload.intent}
|
||||
isImplemented={false}
|
||||
/>,
|
||||
{
|
||||
theme: currentThemeValue,
|
||||
}
|
||||
);
|
||||
return {
|
||||
status: 'error' as const,
|
||||
error: {
|
||||
code: UIActionErrorCode.UNSUPPORTED_ACTION,
|
||||
message: 'Intent handling is not yet implemented',
|
||||
details: actionEvent.payload,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleSizeChangeCase = async (
|
||||
actionEvent: UISizeChange
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Size change handled',
|
||||
data: actionEvent.payload,
|
||||
};
|
||||
};
|
||||
|
||||
const handleIframeReadyCase = async (
|
||||
actionEvent: UILifecycleIframeReady
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
console.log('[MCP-UI] Iframe ready to receive messages');
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Iframe is ready to receive messages',
|
||||
data: actionEvent.payload,
|
||||
};
|
||||
};
|
||||
|
||||
const handleRequestDataCase = async (
|
||||
actionEvent: UIRequestData
|
||||
): Promise<UIActionHandlerResult> => {
|
||||
const { messageId, payload } = actionEvent;
|
||||
const { requestType, params } = payload;
|
||||
console.log('[MCP-UI] Data request received:', { messageId, requestType, params });
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: `Data request received: ${requestType}`,
|
||||
data: {
|
||||
messageId,
|
||||
requestType,
|
||||
params,
|
||||
response: { status: 'acknowledged' },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
switch (actionEvent.type) {
|
||||
case 'tool':
|
||||
result = await handleToolCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'prompt':
|
||||
result = await handlePromptCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
result = await handleLinkCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'notify':
|
||||
result = await handleNotifyCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'intent':
|
||||
result = await handleIntentCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'ui-size-change':
|
||||
result = await handleSizeChangeCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'ui-lifecycle-iframe-ready':
|
||||
result = await handleIframeReadyCase(actionEvent);
|
||||
break;
|
||||
|
||||
case 'ui-request-data':
|
||||
result = await handleRequestDataCase(actionEvent);
|
||||
break;
|
||||
|
||||
default: {
|
||||
const _exhaustiveCheck: never = actionEvent;
|
||||
console.error('Unhandled action type:', _exhaustiveCheck);
|
||||
result = {
|
||||
status: 'error',
|
||||
error: {
|
||||
code: UIActionErrorCode.UNKNOWN_ACTION,
|
||||
message: `Unknown action type`,
|
||||
details: actionEvent,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MCP-UI] Unexpected error:', error);
|
||||
result = {
|
||||
status: 'error',
|
||||
error: {
|
||||
code: UIActionErrorCode.UNKNOWN_ACTION,
|
||||
message: 'An unexpected error occurred',
|
||||
details: error instanceof Error ? error.stack : error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
console.error('[MCP-UI] Action failed:', result);
|
||||
} else {
|
||||
console.log('[MCP-UI] Action succeeded:', result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-3 p-4 border border-borderSubtle rounded-lg bg-background-muted">
|
||||
<div className="overflow-hidden rounded-sm">
|
||||
<UIResourceRenderer
|
||||
resource={content.resource}
|
||||
onUIAction={handleUIAction}
|
||||
htmlProps={
|
||||
{
|
||||
// Remove autoResizeIframe as it's not in the type definition
|
||||
}
|
||||
}
|
||||
htmlProps={{
|
||||
autoResizeIframe: {
|
||||
height: true,
|
||||
width: false, // set to false to allow for responsive design
|
||||
},
|
||||
sandboxPermissions: 'allow-forms', // enabled for experimentation, is spread into underlying iframe defaults
|
||||
iframeRenderData: {
|
||||
// iframeRenderData allows us to pass data down to MCP-UIs
|
||||
// MPC-UIs might find stuff like host and theme for conditional rendering
|
||||
// usage of this is experimental, leaving in place for demos
|
||||
host: 'goose',
|
||||
theme: currentThemeValue,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import { ChevronRight, FlaskConical, LoaderCircle } from 'lucide-react';
|
||||
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
|
||||
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
||||
import { isUIResource } from '@mcp-ui/client';
|
||||
|
||||
interface ToolCallWithResponseProps {
|
||||
isCancelledMessage: boolean;
|
||||
@@ -16,6 +17,7 @@ interface ToolCallWithResponseProps {
|
||||
toolResponse?: ToolResponseMessageContent;
|
||||
notifications?: NotificationEvent[];
|
||||
isStreamingMessage?: boolean;
|
||||
append?: (value: string) => void; // Function to append messages to the chat
|
||||
}
|
||||
|
||||
export default function ToolCallWithResponse({
|
||||
@@ -24,6 +26,7 @@ export default function ToolCallWithResponse({
|
||||
toolResponse,
|
||||
notifications,
|
||||
isStreamingMessage = false,
|
||||
append,
|
||||
}: ToolCallWithResponseProps) {
|
||||
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
|
||||
if (!toolCall) {
|
||||
@@ -50,10 +53,10 @@ export default function ToolCallWithResponse({
|
||||
{/* MCP UI — Inline */}
|
||||
{toolResponse?.toolResult?.value &&
|
||||
toolResponse.toolResult.value.map((content, index) => {
|
||||
if (content.type === 'resource' && content.resource.uri?.startsWith('ui://')) {
|
||||
if (isUIResource(content)) {
|
||||
return (
|
||||
<div key={`${content.type}-${index}`} className="mt-3">
|
||||
<MCPUIResourceRenderer content={content} />
|
||||
<MCPUIResourceRenderer content={content} 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">
|
||||
|
||||
@@ -1070,6 +1070,17 @@ ipcMain.on('react-ready', () => {
|
||||
console.log('[main] React ready - window is prepared for deep links');
|
||||
});
|
||||
|
||||
// Handle external URL opening
|
||||
ipcMain.handle('open-external', async (_event, url: string) => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error opening external URL:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle directory chooser
|
||||
ipcMain.handle('directory-chooser', (_event) => {
|
||||
return openDirectoryDialog();
|
||||
|
||||
@@ -96,6 +96,8 @@ type ElectronAPI = {
|
||||
// Functions for image pasting
|
||||
saveDataUrlToTemp: (dataUrl: string, uniqueId: string) => Promise<SaveDataUrlResponse>;
|
||||
deleteTempFile: (filePath: string) => void;
|
||||
// Function for opening external URLs securely
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
// Function to serve temp images
|
||||
getTempImage: (filePath: string) => Promise<string | null>;
|
||||
// Update-related functions
|
||||
@@ -208,6 +210,9 @@ const electronAPI: ElectronAPI = {
|
||||
deleteTempFile: (filePath: string): void => {
|
||||
ipcRenderer.send('delete-temp-file', filePath);
|
||||
},
|
||||
openExternal: (url: string): Promise<void> => {
|
||||
return ipcRenderer.invoke('open-external', url);
|
||||
},
|
||||
getTempImage: (filePath: string): Promise<string | null> => {
|
||||
return ipcRenderer.invoke('get-temp-image', filePath);
|
||||
},
|
||||
|
||||
@@ -12,5 +12,5 @@ export default defineConfig({
|
||||
|
||||
build: {
|
||||
target: 'esnext'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user