Skip to Content
PlatformUIMessenger Protocol

UIMessenger Protocol

Fractal’s UIMessenger protocol is designed to allow embedded UI components to securely exchange data with the host application.

Initialization Handshake

UIMessenger prefers to send iframe-parent communication through a dedicated MessageChannel rather than directly through window when possible. The first step of the protocol is then to create a shared MessageChannel.

First, the embedded component pings the parent:

window.parent.postMessage({ type: "UI-lifecycle-iframe-init-handshake" }, '*');

When the parent receives the message, it opens a dedicated MessageChannel and sends an attached MessagePort to the iframe along with any data to render.

const channel = new MessageChannel(); //... iframe.contentWindow!.postMessage({ type: IframeHandshakeMessageTypes.HOST_REPLY_HANDSHAKE, renderData: args.renderData }, '*', [channel.port2]);

The MessageChannel provides two ports - one for the iframe and one for the parent. Both sides wrap their port in a MessagingClient

Internal MessagingClient

The UIMessenger protocol runs on simple RPC primitives under the hood. The MessagingClient implements these primitives and does all of its communication through the dedicated MessageChannel. The MessagingClient allows the following operations:

// Create MessagingClient from a MessagePort const mc = new MessagingClient({ port: channel.port1 }); // Send an asynchronous request, expecting a response back. const response = await mc.request({ type: "some-request-type", payload: {} }) // "fire and forget" - don't expect a response mc.emit({ type: "some-event-type", payload: {} }) // Handle inbound events mc.on("some-message-type", (payload: any) => {})

The UIMessenger uses the following message envelope for passing data

export interface ReplySuccessMessageEnvelope { kind: 'success'; id: string; } export type ReplySuccessMessage<TData extends Record<string, unknown> = Record<string, unknown>> = ReplySuccessMessageEnvelope & TData; export interface ReplyErrorMessage { kind: 'error'; id: string; error: string; } export interface EventMessageEnvelope { kind: 'event'; id: string; } export type EventMessage<TData extends Record<string, unknown> = Record<string, unknown>> = EventMessageEnvelope & TData; export interface RequestMessageEnvelope { kind: 'request'; id: string; } export type RequestMessage<TData extends Record<string, unknown> = Record<string, unknown>> = RequestMessageEnvelope & TData; export type MessageEnvelope<TData extends Record<string, unknown> = Record<string, unknown>> = RequestMessage<TData> | ReplySuccessMessage<TData> | ReplyErrorMessage | EventMessage<TData>;

The data passed within the message envelope is of type UIActionMessage | AgentActionMessage.

export type UIActionResizeMessage = { type: 'resize'; payload: { width: number; height: number; }; }; export type UIActionToolCallMessage = { type: 'tool'; payload: { toolName: string; params: Record<string, unknown>; }; }; export type UIActionPromptMessage = { type: 'prompt'; payload: { prompt: string; }; }; export type UIActionLinkMessage = { type: 'link'; payload: { url: string; }; }; export type UIActionIntentMessage = { type: 'intent'; payload: { intent: string; params: Record<string, unknown>; }; }; export type UIActionNotificationMessage = { type: 'notify'; payload: { message: string; }; }; export type AgentActionListTools = { type: "listTools"; payload: {}; }; export type AgentActionCallTool = { type: "callTool"; payload: { [x: string]: unknown; name: string; _meta?: { [x: string]: unknown; progressToken?: string | number | undefined; } | undefined; arguments?: { [x: string]: unknown; } | undefined; }; }; export type AgentActionQueryDOM = { type: "queryDom"; payload: { selector?: string; }; }; export type AgentActionClick = { type: "click"; payload: { selector: string; }; }; export type AgentActionEnterText = { type: "enterText"; payload: { selector: string; text: string; }; }; export type UIActionMessage = UIActionToolCallMessage | UIActionPromptMessage | UIActionLinkMessage | UIActionIntentMessage | UIActionNotificationMessage | UIActionResizeMessage; export type AgentActionMessage = AgentActionListTools | AgentActionCallTool | AgentActionQueryDOM | AgentActionClick | AgentActionEnterText;

UIMessenger client

The UIMessenger provides a clean interface for the embedded application to communicate with the host.

If using the UIMessenger client directly, then you have access to helpful methods like this:

const u = await initUIMessenger() // Returns the data received during the handshake. const data = u.getRenderData() // Emits a "link" message to the client u.link("https://www.example.com") // Emits a "tool" message to force a tool call (of course this only works if respected by the client) u.tool("sometool", params) // And more!

Additionally, component resizing is handled automatically without any input from the developer.

useUIMessenger hook

This is sugar around the UIMessenger client that you can use in react.

If you call `useUIMessenger, then a UIMessenger client is automatically created, and you get access to data and all key UIMessenger client methods once the handshake completes.

const {data, link, tool} = useUIMessenger()

Note that the first render will have null data even if you expect a value in data, but all of the other methods exist, and will internally block until the handshake completes.