feat(basic-host): URL params, fullscreen mode, unified layout (#266)
* basic-host: handle containerDimensions * feat(basic-host): Unified tool call layout with shared collapsible panels * snapshots * format
|
|
@ -228,12 +228,19 @@ export type AppMessage = McpUiMessageRequest["params"];
|
||||||
export interface AppBridgeCallbacks {
|
export interface AppBridgeCallbacks {
|
||||||
onContextUpdate?: (context: ModelContext | null) => void;
|
onContextUpdate?: (context: ModelContext | null) => void;
|
||||||
onMessage?: (message: AppMessage) => void;
|
onMessage?: (message: AppMessage) => void;
|
||||||
|
onDisplayModeChange?: (mode: "inline" | "fullscreen") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppBridgeOptions {
|
||||||
|
containerDimensions?: { maxHeight?: number; width?: number } | { height: number; width?: number };
|
||||||
|
displayMode?: "inline" | "fullscreen";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newAppBridge(
|
export function newAppBridge(
|
||||||
serverInfo: ServerInfo,
|
serverInfo: ServerInfo,
|
||||||
iframe: HTMLIFrameElement,
|
iframe: HTMLIFrameElement,
|
||||||
callbacks?: AppBridgeCallbacks,
|
callbacks?: AppBridgeCallbacks,
|
||||||
|
options?: AppBridgeOptions,
|
||||||
): AppBridge {
|
): AppBridge {
|
||||||
const serverCapabilities = serverInfo.client.getServerCapabilities();
|
const serverCapabilities = serverInfo.client.getServerCapabilities();
|
||||||
const appBridge = new AppBridge(serverInfo.client, IMPLEMENTATION, {
|
const appBridge = new AppBridge(serverInfo.client, IMPLEMENTATION, {
|
||||||
|
|
@ -242,6 +249,12 @@ export function newAppBridge(
|
||||||
serverResources: serverCapabilities?.resources,
|
serverResources: serverCapabilities?.resources,
|
||||||
// Declare support for model context updates
|
// Declare support for model context updates
|
||||||
updateModelContext: { text: {} },
|
updateModelContext: { text: {} },
|
||||||
|
}, {
|
||||||
|
hostContext: {
|
||||||
|
containerDimensions: options?.containerDimensions ?? { maxHeight: 600 },
|
||||||
|
displayMode: options?.displayMode ?? "inline",
|
||||||
|
availableDisplayModes: ["inline", "fullscreen"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register all handlers before calling connect(). The Guest UI can start
|
// Register all handlers before calling connect(). The Guest UI can start
|
||||||
|
|
@ -308,5 +321,18 @@ export function newAppBridge(
|
||||||
iframe.animate([from, to], { duration: 300, easing: "ease-out" });
|
iframe.animate([from, to], { duration: 300, easing: "ease-out" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle display mode change requests from the app
|
||||||
|
appBridge.onrequestdisplaymode = async (params) => {
|
||||||
|
log.info("Display mode request from MCP App:", params);
|
||||||
|
const newMode = params.mode === "fullscreen" ? "fullscreen" : "inline";
|
||||||
|
// Update host context and notify the app
|
||||||
|
appBridge.sendHostContextChange({
|
||||||
|
displayMode: newMode,
|
||||||
|
});
|
||||||
|
// Notify the host UI (via callback)
|
||||||
|
callbacks?.onDisplayModeChange?.(newMode);
|
||||||
|
return { mode: newMode };
|
||||||
|
};
|
||||||
|
|
||||||
return appBridge;
|
return appBridge;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@
|
||||||
|
|
||||||
.toolCallInfoPanel {
|
.toolCallInfoPanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
animation: slideDown 0.3s ease-out;
|
animation: slideDown 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,57 +81,6 @@
|
||||||
from { opacity: 0; transform: translateY(-12px); }
|
from { opacity: 0; transform: translateY(-12px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputInfoPanel {
|
|
||||||
display: flex;
|
|
||||||
flex: 3;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.toolName {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeButton {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #e0e0e0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: #666;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #d0d0d0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.outputInfoPanel {
|
|
||||||
flex: 4;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appOutputPanel {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appHeader {
|
.appHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -166,16 +116,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jsonBlock {
|
|
||||||
flex-grow: 1;
|
|
||||||
min-height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIframePanel {
|
.appIframePanel {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
||||||
|
|
@ -186,6 +126,62 @@
|
||||||
border: 3px dashed #888;
|
border: 3px dashed #888;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: none;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* Hide collapsible panels in fullscreen */
|
||||||
|
.collapsiblePanel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appToolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.collapsiblePanel {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreenButton {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsiblePanel {
|
.collapsiblePanel {
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,20 @@ interface HostProps {
|
||||||
type ToolCallEntry = ToolCallInfo & { id: number };
|
type ToolCallEntry = ToolCallInfo & { id: number };
|
||||||
let nextToolCallId = 0;
|
let nextToolCallId = 0;
|
||||||
|
|
||||||
|
// Parse URL query params for debugging: ?server=name&tool=name&call=true
|
||||||
|
function getQueryParams() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return {
|
||||||
|
server: params.get("server"),
|
||||||
|
tool: params.get("tool"),
|
||||||
|
call: params.get("call") === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function Host({ serversPromise }: HostProps) {
|
function Host({ serversPromise }: HostProps) {
|
||||||
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
||||||
const [destroyingIds, setDestroyingIds] = useState<Set<number>>(new Set());
|
const [destroyingIds, setDestroyingIds] = useState<Set<number>>(new Set());
|
||||||
|
const queryParams = useMemo(() => getQueryParams(), []);
|
||||||
|
|
||||||
const requestClose = (id: number) => {
|
const requestClose = (id: number) => {
|
||||||
setDestroyingIds((s) => new Set(s).add(id));
|
setDestroyingIds((s) => new Set(s).add(id));
|
||||||
|
|
@ -85,6 +96,9 @@ function Host({ serversPromise }: HostProps) {
|
||||||
<CallToolPanel
|
<CallToolPanel
|
||||||
serversPromise={serversPromise}
|
serversPromise={serversPromise}
|
||||||
addToolCall={(info) => setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])}
|
addToolCall={(info) => setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])}
|
||||||
|
initialServer={queryParams.server}
|
||||||
|
initialTool={queryParams.tool}
|
||||||
|
autoCall={queryParams.call}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -95,11 +109,15 @@ function Host({ serversPromise }: HostProps) {
|
||||||
interface CallToolPanelProps {
|
interface CallToolPanelProps {
|
||||||
serversPromise: Promise<ServerInfo[]>;
|
serversPromise: Promise<ServerInfo[]>;
|
||||||
addToolCall: (info: ToolCallInfo) => void;
|
addToolCall: (info: ToolCallInfo) => void;
|
||||||
|
initialServer?: string | null;
|
||||||
|
initialTool?: string | null;
|
||||||
|
autoCall?: boolean;
|
||||||
}
|
}
|
||||||
function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
function CallToolPanel({ serversPromise, addToolCall, initialServer, initialTool, autoCall }: CallToolPanelProps) {
|
||||||
const [selectedServer, setSelectedServer] = useState<ServerInfo | null>(null);
|
const [selectedServer, setSelectedServer] = useState<ServerInfo | null>(null);
|
||||||
const [selectedTool, setSelectedTool] = useState("");
|
const [selectedTool, setSelectedTool] = useState("");
|
||||||
const [inputJson, setInputJson] = useState("{}");
|
const [inputJson, setInputJson] = useState("{}");
|
||||||
|
const [hasAutoCalledRef] = useState({ called: false });
|
||||||
|
|
||||||
// Filter out app-only tools, prioritize tools with UIs
|
// Filter out app-only tools, prioritize tools with UIs
|
||||||
const toolNames = selectedServer
|
const toolNames = selectedServer
|
||||||
|
|
@ -118,16 +136,21 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
||||||
}
|
}
|
||||||
}, [inputJson]);
|
}, [inputJson]);
|
||||||
|
|
||||||
const handleServerSelect = (server: ServerInfo) => {
|
const handleServerSelect = (server: ServerInfo, preferredTool?: string) => {
|
||||||
setSelectedServer(server);
|
setSelectedServer(server);
|
||||||
// Filter out app-only tools, prioritize tools with UIs
|
// Filter out app-only tools, prioritize tools with UIs
|
||||||
const visibleTools = Array.from(server.tools.values())
|
const visibleTools = Array.from(server.tools.values())
|
||||||
.filter((tool) => isToolVisibleToModel(tool))
|
.filter((tool) => isToolVisibleToModel(tool))
|
||||||
.sort(compareTools);
|
.sort(compareTools);
|
||||||
const firstTool = visibleTools[0]?.name ?? "";
|
|
||||||
setSelectedTool(firstTool);
|
// Use preferred tool if it exists and is visible, otherwise first visible tool
|
||||||
|
const targetTool = preferredTool && visibleTools.some(t => t.name === preferredTool)
|
||||||
|
? preferredTool
|
||||||
|
: visibleTools[0]?.name ?? "";
|
||||||
|
|
||||||
|
setSelectedTool(targetTool);
|
||||||
// Set input JSON to tool defaults (if any)
|
// Set input JSON to tool defaults (if any)
|
||||||
setInputJson(getToolDefaults(server.tools.get(firstTool)));
|
setInputJson(getToolDefaults(server.tools.get(targetTool)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolSelect = (toolName: string) => {
|
const handleToolSelect = (toolName: string) => {
|
||||||
|
|
@ -136,10 +159,21 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
||||||
setInputJson(getToolDefaults(selectedServer?.tools.get(toolName)));
|
setInputJson(getToolDefaults(selectedServer?.tools.get(toolName)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
// Submit with optional override for server/tool (used by auto-call)
|
||||||
if (!selectedServer) return;
|
const handleSubmit = (overrideServer?: ServerInfo, overrideTool?: string) => {
|
||||||
const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson));
|
const server = overrideServer ?? selectedServer;
|
||||||
|
const tool = overrideTool ?? selectedTool;
|
||||||
|
if (!server) return;
|
||||||
|
|
||||||
|
const toolCallInfo = callTool(server, tool, JSON.parse(inputJson));
|
||||||
addToolCall(toolCallInfo);
|
addToolCall(toolCallInfo);
|
||||||
|
|
||||||
|
// Update URL for easy refresh/sharing (without triggering navigation)
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("server", server.name);
|
||||||
|
url.searchParams.set("tool", tool);
|
||||||
|
url.searchParams.set("call", "true"); // Auto-call on refresh
|
||||||
|
history.replaceState(null, "", url.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -148,7 +182,17 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
||||||
<label>
|
<label>
|
||||||
Server
|
Server
|
||||||
<Suspense fallback={<select disabled><option>Loading...</option></select>}>
|
<Suspense fallback={<select disabled><option>Loading...</option></select>}>
|
||||||
<ServerSelect serversPromise={serversPromise} onSelect={handleServerSelect} />
|
<ServerSelect
|
||||||
|
serversPromise={serversPromise}
|
||||||
|
onSelect={handleServerSelect}
|
||||||
|
initialServer={initialServer}
|
||||||
|
initialTool={initialTool}
|
||||||
|
autoCall={autoCall && !hasAutoCalledRef.called}
|
||||||
|
onAutoCall={(server, tool) => {
|
||||||
|
hasAutoCalledRef.called = true;
|
||||||
|
handleSubmit(server, tool);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -184,17 +228,47 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
||||||
// ServerSelect calls use() and renders the server <select>
|
// ServerSelect calls use() and renders the server <select>
|
||||||
interface ServerSelectProps {
|
interface ServerSelectProps {
|
||||||
serversPromise: Promise<ServerInfo[]>;
|
serversPromise: Promise<ServerInfo[]>;
|
||||||
onSelect: (server: ServerInfo) => void;
|
onSelect: (server: ServerInfo, toolName?: string) => void;
|
||||||
|
initialServer?: string | null;
|
||||||
|
initialTool?: string | null;
|
||||||
|
autoCall?: boolean;
|
||||||
|
onAutoCall?: (server: ServerInfo, tool: string) => void;
|
||||||
}
|
}
|
||||||
function ServerSelect({ serversPromise, onSelect }: ServerSelectProps) {
|
function ServerSelect({ serversPromise, onSelect, initialServer, initialTool, autoCall, onAutoCall }: ServerSelectProps) {
|
||||||
const servers = use(serversPromise);
|
const servers = use(serversPromise);
|
||||||
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
// Initialize with the correct server/tool when servers are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (servers.length > selectedIndex) {
|
if (hasInitialized || servers.length === 0) return;
|
||||||
onSelect(servers[selectedIndex]);
|
|
||||||
|
// Find initial server index if specified
|
||||||
|
let idx = 0;
|
||||||
|
if (initialServer) {
|
||||||
|
const foundIdx = servers.findIndex(s => s.name === initialServer);
|
||||||
|
if (foundIdx >= 0) idx = foundIdx;
|
||||||
}
|
}
|
||||||
}, [servers]);
|
|
||||||
|
const server = servers[idx];
|
||||||
|
setSelectedIndex(idx);
|
||||||
|
|
||||||
|
// Find the tool to use
|
||||||
|
const visibleTools = Array.from(server.tools.values())
|
||||||
|
.filter((tool) => isToolVisibleToModel(tool))
|
||||||
|
.sort(compareTools);
|
||||||
|
const targetTool = initialTool && visibleTools.some(t => t.name === initialTool)
|
||||||
|
? initialTool
|
||||||
|
: visibleTools[0]?.name ?? "";
|
||||||
|
|
||||||
|
onSelect(server, targetTool);
|
||||||
|
setHasInitialized(true);
|
||||||
|
|
||||||
|
// Auto-call after initial selection if requested
|
||||||
|
if (autoCall && targetTool) {
|
||||||
|
onAutoCall?.(server, targetTool);
|
||||||
|
}
|
||||||
|
}, [servers, hasInitialized, initialServer, initialTool, autoCall, onSelect, onAutoCall]);
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
return <select disabled><option>No servers configured</option></select>;
|
return <select disabled><option>No servers configured</option></select>;
|
||||||
|
|
@ -233,33 +307,14 @@ function ToolCallInfoPanel({ toolCallInfo, isDestroying, onRequestClose, onClose
|
||||||
}
|
}
|
||||||
}, [isDestroying, isApp, onCloseComplete]);
|
}, [isDestroying, isApp, onCloseComplete]);
|
||||||
|
|
||||||
|
const inputJson = JSON.stringify(toolCallInfo.input, null, 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.toolCallInfoPanel}
|
className={styles.toolCallInfoPanel}
|
||||||
style={isDestroying ? { opacity: 0.5, pointerEvents: "none" } : undefined}
|
style={isDestroying ? { opacity: 0.5, pointerEvents: "none" } : undefined}
|
||||||
>
|
>
|
||||||
{/* For non-app tools, show input/output side by side */}
|
{/* Row 1: Header with server:tool name and close button */}
|
||||||
{!isApp && (
|
|
||||||
<div className={styles.inputInfoPanel}>
|
|
||||||
<h2>
|
|
||||||
<span>{toolCallInfo.serverInfo.name}</span>
|
|
||||||
<span className={styles.toolName}>{toolCallInfo.tool.name}</span>
|
|
||||||
{onRequestClose && !isDestroying && (
|
|
||||||
<button
|
|
||||||
className={styles.closeButton}
|
|
||||||
onClick={onRequestClose}
|
|
||||||
title="Close"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<JsonBlock value={toolCallInfo.input} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={isApp ? styles.appOutputPanel : styles.outputInfoPanel}>
|
|
||||||
{/* For apps, show header above the app: ServerName:tool_name */}
|
|
||||||
{isApp && (
|
|
||||||
<div className={styles.appHeader}>
|
<div className={styles.appHeader}>
|
||||||
<span>{toolCallInfo.serverInfo.name}:<span className={styles.toolName}>{toolCallInfo.tool.name}</span></span>
|
<span>{toolCallInfo.serverInfo.name}:<span className={styles.toolName}>{toolCallInfo.tool.name}</span></span>
|
||||||
{onRequestClose && !isDestroying && (
|
{onRequestClose && !isDestroying && (
|
||||||
|
|
@ -272,31 +327,30 @@ function ToolCallInfoPanel({ toolCallInfo, isDestroying, onRequestClose, onClose
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Row 2: Tool Input */}
|
||||||
|
<CollapsiblePanel icon="📥" label="Tool Input" content={inputJson} />
|
||||||
|
|
||||||
|
{/* Row 3: App iframe (if app) */}
|
||||||
|
{isApp && (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback="Loading...">
|
<Suspense fallback="Loading...">
|
||||||
{
|
<AppIFramePanel
|
||||||
isApp
|
|
||||||
? <AppIFramePanel
|
|
||||||
toolCallInfo={toolCallInfo}
|
toolCallInfo={toolCallInfo}
|
||||||
isDestroying={isDestroying}
|
isDestroying={isDestroying}
|
||||||
onTeardownComplete={onCloseComplete}
|
onTeardownComplete={onCloseComplete}
|
||||||
/>
|
/>
|
||||||
: <ToolResultPanel toolCallInfo={toolCallInfo} />
|
</Suspense>
|
||||||
}
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Row 4: Tool Result */}
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback="Loading result...">
|
||||||
|
<ToolResultPanel toolCallInfo={toolCallInfo} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function JsonBlock({ value }: { value: object }) {
|
|
||||||
return (
|
|
||||||
<pre className={styles.jsonBlock}>
|
|
||||||
<code>{JSON.stringify(value, null, 2)}</code>
|
|
||||||
</pre>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,8 +401,8 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||||
const appBridgeRef = useRef<ReturnType<typeof newAppBridge> | null>(null);
|
const appBridgeRef = useRef<ReturnType<typeof newAppBridge> | null>(null);
|
||||||
const [modelContext, setModelContext] = useState<ModelContext | null>(null);
|
const [modelContext, setModelContext] = useState<ModelContext | null>(null);
|
||||||
const [toolResult, setToolResult] = useState<object | null>(null);
|
|
||||||
const [messages, setMessages] = useState<AppMessage[]>([]);
|
const [messages, setMessages] = useState<AppMessage[]>([]);
|
||||||
|
const [displayMode, setDisplayMode] = useState<"inline" | "fullscreen">("inline");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iframe = iframeRef.current!;
|
const iframe = iframeRef.current!;
|
||||||
|
|
@ -365,6 +419,11 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe, {
|
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe, {
|
||||||
onContextUpdate: setModelContext,
|
onContextUpdate: setModelContext,
|
||||||
onMessage: (msg) => setMessages((prev) => [...prev, msg]),
|
onMessage: (msg) => setMessages((prev) => [...prev, msg]),
|
||||||
|
onDisplayModeChange: setDisplayMode,
|
||||||
|
}, {
|
||||||
|
// Provide container dimensions - maxHeight for flexible sizing
|
||||||
|
containerDimensions: { maxHeight: 600 },
|
||||||
|
displayMode: "inline",
|
||||||
});
|
});
|
||||||
appBridgeRef.current = appBridge;
|
appBridgeRef.current = appBridge;
|
||||||
initializeApp(iframe, appBridge, toolCallInfo);
|
initializeApp(iframe, appBridge, toolCallInfo);
|
||||||
|
|
@ -372,8 +431,6 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track tool result for display
|
|
||||||
toolCallInfo.resultPromise.then(setToolResult).catch(() => {});
|
|
||||||
}, [toolCallInfo]);
|
}, [toolCallInfo]);
|
||||||
|
|
||||||
// Graceful teardown: wait for guest to respond before unmounting
|
// Graceful teardown: wait for guest to respond before unmounting
|
||||||
|
|
@ -421,9 +478,6 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
: "";
|
: "";
|
||||||
const fullContext = [contextText, contextJson].filter(Boolean).join("\n\n");
|
const fullContext = [contextText, contextJson].filter(Boolean).join("\n\n");
|
||||||
|
|
||||||
const inputJson = JSON.stringify(toolCallInfo.input, null, 2);
|
|
||||||
const resultJson = toolResult ? JSON.stringify(toolResult, null, 2) : null;
|
|
||||||
|
|
||||||
// Format messages
|
// Format messages
|
||||||
const formatMessage = (m: AppMessage) => {
|
const formatMessage = (m: AppMessage) => {
|
||||||
const content = m.content.map(formatContentBlock).join("\n");
|
const content = m.content.map(formatContentBlock).join("\n");
|
||||||
|
|
@ -431,13 +485,13 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
||||||
};
|
};
|
||||||
const messagesText = messages.map(formatMessage).join("\n\n");
|
const messagesText = messages.map(formatMessage).join("\n\n");
|
||||||
|
|
||||||
|
const panelClassName = displayMode === "fullscreen"
|
||||||
|
? `${styles.appIframePanel} ${styles.fullscreen}`
|
||||||
|
: styles.appIframePanel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.appIframePanel}>
|
<div className={panelClassName}>
|
||||||
<CollapsiblePanel icon="📥" label="Tool Input" content={inputJson} />
|
|
||||||
<iframe ref={iframeRef} />
|
<iframe ref={iframeRef} />
|
||||||
{resultJson && (
|
|
||||||
<CollapsiblePanel icon="📤" label="Tool Result" content={resultJson} />
|
|
||||||
)}
|
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
icon="💬"
|
icon="💬"
|
||||||
|
|
@ -459,7 +513,8 @@ interface ToolResultPanelProps {
|
||||||
}
|
}
|
||||||
function ToolResultPanel({ toolCallInfo }: ToolResultPanelProps) {
|
function ToolResultPanel({ toolCallInfo }: ToolResultPanelProps) {
|
||||||
const result = use(toolCallInfo.resultPromise);
|
const result = use(toolCallInfo.resultPromise);
|
||||||
return <JsonBlock value={result} />;
|
const resultJson = JSON.stringify(result, null, 2);
|
||||||
|
return <CollapsiblePanel icon="📤" label="Tool Result" content={resultJson} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |