diff --git a/examples/express-react-todo/src/tools/get.tsx b/examples/express-react-todo/src/tools/get.tsx index ee40d99..b418f84 100644 --- a/examples/express-react-todo/src/tools/get.tsx +++ b/examples/express-react-todo/src/tools/get.tsx @@ -50,5 +50,8 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + }, }, ); diff --git a/examples/hono-todo/src/tools/get.tsx b/examples/hono-todo/src/tools/get.tsx index 4124b93..9a4cc22 100644 --- a/examples/hono-todo/src/tools/get.tsx +++ b/examples/hono-todo/src/tools/get.tsx @@ -54,5 +54,9 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + toolResponseMetadata: { timestamp: new Date().toISOString() }, + }, }, ); diff --git a/packages/chapplin/src/preview.ts b/packages/chapplin/src/preview.ts new file mode 100644 index 0000000..0c306cd --- /dev/null +++ b/packages/chapplin/src/preview.ts @@ -0,0 +1,97 @@ +import { + type DisplayMode, + SET_GLOBALS_EVENT_TYPE, + type OpenAiGlobals, +} from "./openai.js"; + +export type Preview = { + toolInput?: Record; + toolOutput?: Record; + toolResponseMetadata?: Record; +}; + +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 | 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; + // Dispatch event to notify subscribers (like useWidgetState hook) + window.dispatchEvent( + new CustomEvent<{ globals: Partial }>( + SET_GLOBALS_EVENT_TYPE, + { detail: { globals: { widgetState } } }, + ), + ); + }, + }; +} diff --git a/packages/chapplin/src/tool-hono.ts b/packages/chapplin/src/tool-hono.ts index 13d95ee..9bf5856 100644 --- a/packages/chapplin/src/tool-hono.ts +++ b/packages/chapplin/src/tool-hono.ts @@ -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 }); @@ -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); } diff --git a/packages/chapplin/src/tool-preact.ts b/packages/chapplin/src/tool-preact.ts index 516324a..3e01bcf 100644 --- a/packages/chapplin/src/tool-preact.ts +++ b/packages/chapplin/src/tool-preact.ts @@ -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 }; +type Widget = { + app: ComponentType; + preview?: Preview; +}; const hooks = createGlobalGetterHooks({ useState, useEffect }); @@ -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); } diff --git a/packages/chapplin/src/tool-react.ts b/packages/chapplin/src/tool-react.ts index 8453680..b9cacf7 100644 --- a/packages/chapplin/src/tool-react.ts +++ b/packages/chapplin/src/tool-react.ts @@ -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 }; +type Widget = { + app: ComponentType; + preview?: Preview; +}; const hooks = createGlobalGetterHooks({ useState, useEffect }); @@ -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 })); } diff --git a/packages/chapplin/src/tool-solid.ts b/packages/chapplin/src/tool-solid.ts index 5b34a4b..b5948b9 100644 --- a/packages/chapplin/src/tool-solid.ts +++ b/packages/chapplin/src/tool-solid.ts @@ -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 }; +type Widget = { + app: Component; + preview?: Preview; +}; export function defineTool( _name: unknown, @@ -12,6 +16,9 @@ export function defineTool( widget?: Widget, ): void { if (!widget) return; + + initializePreview(widget.preview); + const container = document.getElementById("app"); if (container) render( diff --git a/packages/chapplin/src/tool.ts b/packages/chapplin/src/tool.ts index 19c4f8f..4454446 100644 --- a/packages/chapplin/src/tool.ts +++ b/packages/chapplin/src/tool.ts @@ -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; + toolOutput?: Shape; + toolResponseMetadata?: OutputMeta extends undefined + ? UnknownObject + : OutputMeta; + }; }, ): Tool, Shape, OutputMeta> { type TypedTool = Tool, Shape, OutputMeta>; diff --git a/packages/create-chapplin/templates/hono/src/tools/get.tsx b/packages/create-chapplin/templates/hono/src/tools/get.tsx index ee40d99..b418f84 100644 --- a/packages/create-chapplin/templates/hono/src/tools/get.tsx +++ b/packages/create-chapplin/templates/hono/src/tools/get.tsx @@ -50,5 +50,8 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + }, }, ); diff --git a/packages/create-chapplin/templates/preact/src/tools/get.tsx b/packages/create-chapplin/templates/preact/src/tools/get.tsx index ee40d99..b418f84 100644 --- a/packages/create-chapplin/templates/preact/src/tools/get.tsx +++ b/packages/create-chapplin/templates/preact/src/tools/get.tsx @@ -50,5 +50,8 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + }, }, ); diff --git a/packages/create-chapplin/templates/react/src/tools/get.tsx b/packages/create-chapplin/templates/react/src/tools/get.tsx index ee40d99..b418f84 100644 --- a/packages/create-chapplin/templates/react/src/tools/get.tsx +++ b/packages/create-chapplin/templates/react/src/tools/get.tsx @@ -50,5 +50,8 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + }, }, ); diff --git a/packages/create-chapplin/templates/solid/src/tools/get.tsx b/packages/create-chapplin/templates/solid/src/tools/get.tsx index 8ff0292..4672c36 100644 --- a/packages/create-chapplin/templates/solid/src/tools/get.tsx +++ b/packages/create-chapplin/templates/solid/src/tools/get.tsx @@ -50,5 +50,8 @@ export default defineTool( ), + preview: { + toolOutput: { todos }, + }, }, );