Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/express-react-todo/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
},
},
);
4 changes: 4 additions & 0 deletions examples/hono-todo/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,9 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
toolResponseMetadata: { timestamp: new Date().toISOString() },
},
},
);
97 changes: 97 additions & 0 deletions packages/chapplin/src/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
type DisplayMode,
SET_GLOBALS_EVENT_TYPE,
type OpenAiGlobals,
} from "./openai.js";

export type Preview = {
toolInput?: Record<string, unknown>;
toolOutput?: Record<string, unknown>;
toolResponseMetadata?: Record<string, unknown>;
};

export type PreviewDefaults = {
theme?: "light" | "dark";
locale?: string;
maxHeight?: number;
displayMode?: DisplayMode;
};

const defaultPreviewGlobals: PreviewDefaults = {
theme: "light",
locale: "en-US",
maxHeight: 600,
displayMode: "inline",
};

/**
* Check if running in development mode.
* Returns false in production to prevent preview stubs from being used.
*/
function isDevMode(): boolean {
try {
// Vite injects import.meta.env.DEV
// biome-ignore lint/suspicious/noExplicitAny: Vite-specific property
return (import.meta as any).env?.DEV === true;
} catch {
// Fallback: assume production if import.meta.env is not available
return false;
}
}

/**
* Initialize window.openai with preview data for local development.
* Only initializes in development mode when preview data is provided
* and window.openai is not already set by the host.
*/
export function initializePreview(
preview?: Preview,
defaults?: PreviewDefaults,
): void {
// Only initialize preview in development mode
if (!preview || window.openai || !isDevMode()) return;

const config = { ...defaultPreviewGlobals, ...defaults };

// Create a mutable state object for widgetState
let widgetState: Record<string, unknown> | null = null;

window.openai = {
theme: config.theme ?? "light",
userAgent: {
device: { type: "desktop" },
capabilities: { hover: true, touch: false },
},
locale: config.locale ?? "en-US",
maxHeight: config.maxHeight ?? 600,
displayMode: config.displayMode ?? "inline",
safeArea: { insets: { top: 0, bottom: 0, left: 0, right: 0 } },
toolInput: preview.toolInput ?? {},
toolOutput: preview.toolOutput ?? null,
toolResponseMetadata: preview.toolResponseMetadata ?? null,
get widgetState() {
return widgetState;
},
// Stub API methods for preview
callTool: async () => {
throw new Error("callTool is not available in preview mode");
},
sendFollowUpMessage: async () => {
throw new Error("sendFollowUpMessage is not available in preview mode");
},
openExternal: () => {
throw new Error("openExternal is not available in preview mode");
},
requestDisplayMode: async () => ({ mode: config.displayMode ?? "inline" }),
setWidgetState: async (state) => {
widgetState = state as Record<string, unknown>;
// Dispatch event to notify subscribers (like useWidgetState hook)
window.dispatchEvent(
new CustomEvent<{ globals: Partial<OpenAiGlobals> }>(
SET_GLOBALS_EVENT_TYPE,
{ detail: { globals: { widgetState } } },
),
);
},
};
}
9 changes: 8 additions & 1 deletion packages/chapplin/src/tool-hono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import type { Child, JSXNode } from "hono/jsx";
import { jsx, render, useEffect, useState } from "hono/jsx/dom";
import { createGlobalGetterHooks } from "./client.js";
import type { OpenAiGlobals } from "./openai.js";
import { type Preview, initializePreview } from "./preview.js";

type Widget = { app: (props: OpenAiGlobals) => Child };
type Widget = {
app: (props: OpenAiGlobals) => Child;
preview?: Preview;
};
type Component = (props: unknown) => JSXNode;

const hooks = createGlobalGetterHooks({ useState, useEffect });
Expand All @@ -15,6 +19,9 @@ export function defineTool(
widget?: Widget,
): void {
if (!widget) return;

initializePreview(widget.preview);

const container = document.getElementById("app");
if (container) render(jsx(App as Component, { app: widget.app }), container);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/chapplin/src/tool-preact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { useEffect, useState } from "preact/hooks";
import { jsx } from "preact/jsx-runtime";
import { createGlobalGetterHooks } from "./client.js";
import type { OpenAiGlobals } from "./openai.js";
import { type Preview, initializePreview } from "./preview.js";

type Widget = { app: ComponentType<OpenAiGlobals> };
type Widget = {
app: ComponentType<OpenAiGlobals>;
preview?: Preview;
};

const hooks = createGlobalGetterHooks({ useState, useEffect });

Expand All @@ -16,6 +20,9 @@ export function defineTool(
widget?: Widget,
): void {
if (!widget) return;

initializePreview(widget.preview);

const container = document.getElementById("app");
if (container) render(jsx(App, { app: widget.app }), container);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/chapplin/src/tool-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { jsx } from "react/jsx-runtime";
import { createRoot } from "react-dom/client";
import { createGlobalGetterHooks } from "./client.js";
import type { OpenAiGlobals } from "./openai.js";
import { type Preview, initializePreview } from "./preview.js";

type Widget = { app: ComponentType<OpenAiGlobals> };
type Widget = {
app: ComponentType<OpenAiGlobals>;
preview?: Preview;
};

const hooks = createGlobalGetterHooks({ useState, useEffect });

Expand All @@ -16,6 +20,9 @@ export function defineTool(
widget?: Widget,
): void {
if (!widget) return;

initializePreview(widget.preview);

const container = document.getElementById("app");
if (container) createRoot(container).render(jsx(App, { app: widget.app }));
}
Expand Down
9 changes: 8 additions & 1 deletion packages/chapplin/src/tool-solid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { type Component, createEffect, createSignal } from "solid-js";
import { createComponent, render } from "solid-js/web";
import { createGlobalGetterHook, createGlobalsSubscribe } from "./client.js";
import type { OpenAiGlobals } from "./openai.js";
import { type Preview, initializePreview } from "./preview.js";

type Widget = { app: Component<OpenAiGlobals> };
type Widget = {
app: Component<OpenAiGlobals>;
preview?: Preview;
};

export function defineTool(
_name: unknown,
Expand All @@ -12,6 +16,9 @@ export function defineTool(
widget?: Widget,
): void {
if (!widget) return;

initializePreview(widget.preview);

const container = document.getElementById("app");
if (container)
render(
Expand Down
11 changes: 11 additions & 0 deletions packages/chapplin/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ export function defineTool<
OutputMeta extends undefined ? UnknownObject : OutputMeta
>,
) => JSXElement;
/**
* Preview data for local development.
* Used to display mock data before MCP response is available.
*/
preview?: {
toolInput?: Shape<InputArgs>;
toolOutput?: Shape<OutputArgs>;
toolResponseMetadata?: OutputMeta extends undefined
? UnknownObject
: OutputMeta;
};
Comment on lines +53 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preview data will be included in production bundles.

The preview field is a welcome addition for development, but since it's part of the widget configuration type without build-time stripping, all preview data (toolInput, toolOutput, toolResponseMetadata) will be bundled into production builds even though they're only used when import.meta.env.DEV is true.

Consider these approaches to prevent preview data from reaching production:

  1. Use a separate file suffix (e.g., *.preview.ts) that can be excluded by bundler configuration
  2. Wrap preview data in if (import.meta.env.DEV) blocks so dead-code elimination can remove it
  3. Document that developers should keep preview data minimal

Additionally, the preview types use Shape<T> which is correct, but the actual Preview type in preview.ts uses Record<string, unknown>, losing the type safety provided by the schemas. Consider tightening the Preview type to preserve schema types if feasible.

Would you like me to generate examples showing how to structure preview data for better tree-shaking, or help document best practices for keeping preview data minimal?

},
): Tool<Shape<InputArgs>, Shape<OutputArgs>, OutputMeta> {
type TypedTool = Tool<Shape<InputArgs>, Shape<OutputArgs>, OutputMeta>;
Expand Down
3 changes: 3 additions & 0 deletions packages/create-chapplin/templates/hono/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
},
},
);
3 changes: 3 additions & 0 deletions packages/create-chapplin/templates/preact/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
},
},
);
3 changes: 3 additions & 0 deletions packages/create-chapplin/templates/react/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
},
},
);
3 changes: 3 additions & 0 deletions packages/create-chapplin/templates/solid/src/tools/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,8 @@ export default defineTool(
</ul>
</div>
),
preview: {
toolOutput: { todos },
},
},
);