import * as React from "react";// ---------------------------------------------------------------------------// The seam between the artifact page and the MCP-Apps shell that renders it.//// The shell (`@executor-js/mcp-apps-shell`) compiles model-written JSX and runs// it inside a sandboxed iframe. It cannot be imported here: the shell already// depends on THIS package for its shadcn component barrel, and turbo rejects// the resulting package cycle outright (it errors, it does not warn). So the// dependency is inverted the way the rest of the console does it — this package// declares the contract, and the app composition roots (`apps/*`, which already// depend on both) provide the implementation at startup.//// A host that never registers a renderer still gets a working artifacts list// and a detail page that explains the artifact cannot be rendered here, rather// than a crash — the same graceful-degradation rule the shared console follows// for any host-specific capability.//// The seam carries a LOADER, not a component. The shell is browser-only by// nature: it imports `@tailwindcss/browser`, which builds a `<style>` element// at import scope, and it harvests `document.styleSheets` to seed the sandboxed// iframe. Under SSR (cloud is TanStack Start) a static import of the shell puts// that module in the server graph and every document request dies with// `ReferenceError: document is not defined` before a component ever renders.// Keeping the seam async means the composition roots hand over a `() =>// import(...)` that only ever runs in the browser, so the shell's own code stays// honestly browser-only instead of being polluted with `typeof document` guards.// ---------------------------------------------------------------------------/** What the page hands the shell: the stored source, and the live host. */export interface ArtifactRendererProps { /** The artifact's stored JSX source. */ readonly code: string; /** Which artifact this is. The source addresses integrations by role, so the * server needs the row to resolve a role to a connection; this is how the id * reaches `execute-action`. */ readonly artifactId: string; /** HTTP-backed MCP host — see `createHttpShellHost` in `./shell-host`. */ readonly host: unknown; /** * Called once, when the artifact has actually painted inside the frame. * * This is what lets the page hold ONE loading surface from navigation until * there is something real to look at, rather than flashing a placeholder per * stage of the boot. The shell owns the fact — the console cannot see inside * a sandboxed frame two documents down — so it reports it; see * `ARTIFACT_RENDERED_TOOL` in the shell package. * * Fires for the error surfaces too. "Rendered" here means presented, not * succeeded: a compile failure is still the moment the user should be looking * at the frame instead of a skeleton, and a signal that fired only on success * would leave every failed artifact under a loading state forever. * * Optional, so a host that does not care simply omits it and nothing in the * chain changes. */ readonly onRendered?: (() => void) | undefined;}export type ArtifactRenderer = React.ComponentType<ArtifactRendererProps>;/** * Resolves the renderer on demand, in the browser only. Apps pass a dynamic * `import()`; the module is never touched during SSR. */export type ArtifactRendererLoader = () => Promise<{ readonly default: ArtifactRenderer }>;const ArtifactRendererContext = React.createContext<ArtifactRendererLoader | null>(null);/** * Provide the shell implementation. Apps mount this above the console shell: * * ```tsx * <ArtifactRendererProvider * loader={() => import("@executor-js/mcp-apps-shell/shell/artifact-renderer")} * > * ``` * * The loader must resolve to a module whose `default` export is the renderer, * which is what `React.lazy` consumes directly. */export function ArtifactRendererProvider( props: React.PropsWithChildren<{ readonly loader: ArtifactRendererLoader }>,) { return ( <ArtifactRendererContext.Provider value={props.loader}> {props.children} </ArtifactRendererContext.Provider> );}/** The registered loader, or `null` on a host that provides none. */export function useArtifactRendererLoader(): ArtifactRendererLoader | null { return React.useContext(ArtifactRendererContext);}/** * The lazy renderer component for the registered loader, or `null` when no host * registered one. * * `React.lazy` is memoized per loader identity: calling it inside render would * mint a fresh component type every pass and remount the shell (throwing away * the compiled iframe) on every parent re-render. */export function useArtifactRenderer(): ArtifactRenderer | null { const loader = useArtifactRendererLoader(); return React.useMemo(() => (loader ? React.lazy(loader) : null), [loader]);}/** * Loaders whose chunk has already been asked for. * * The dedupe is not for the network — a bundler's own module registry already * serves a second `import()` of the same specifier from memory. It is so this * module does not hand React's scheduler a new promise on every render of every * component that preloads, and so the cost of "preload on hover" style callers * is provably zero after the first. * * Keyed by loader identity, matching how `useArtifactRenderer` memoizes * `React.lazy`; the composition roots define their loader at module scope * precisely so that identity is stable for the life of the app. */const preloaded = new WeakSet<ArtifactRendererLoader>();/** * Start fetching the renderer chunk before anything needs to render it. * * ## What this removes * * The renderer arrives through a dynamic `import()`, and until now that import * was first touched when the detail page rendered — which is only after the * artifact row has been fetched. So a cold open serialized two waits the user * saw as two different placeholders: "loading the row", then "loading the code * that draws rows". They are independent, and overlapping them makes the second * free. * * ## Why fire-and-forget is right * * There is nothing to do with the result. Success populates the bundler's * module cache, which is the entire point, and `React.lazy` will find it there. * FAILURE is deliberately swallowed here rather than surfaced: this is a * speculative fetch, the user may never open an artifact, and a chunk that * genuinely cannot load must fail where a user is actually waiting on it — at * the `Suspense` boundary, with the page's own error surface — not as an * unhandled rejection logged from a hover. Retrying is automatic in the sense * that matters: `React.lazy` calls the same loader again and gets a fresh * attempt, because a rejected `import()` is not cached by any bundler. */export function preloadArtifactRenderer(loader: ArtifactRendererLoader | null): void { if (!loader || preloaded.has(loader)) return; preloaded.add(loader); void loader().then( () => undefined, () => { // Let a failed speculative fetch be tried again for real. Without this a // transient network blip during the preload would mark the loader done // and the actual open would never re-attempt from here. preloaded.delete(loader); }, );}/** * Kick the renderer chunk as soon as the mounting surface exists. * * Mounted by the artifacts LIST as well as the detail page: by the time a user * has the gallery on screen, opening one of the cards is the only thing that * page is for, so the chunk is fetched during the time they spend choosing. * That is what turns the renderer's own placeholder into something that never * appears in a warm session, and shortens it to nothing in a cold one. * * An effect rather than a render-phase call, so the fetch is never started * during SSR or a discarded render pass. */export function usePreloadArtifactRenderer(): void { const loader = useArtifactRendererLoader(); React.useEffect(() => { preloadArtifactRenderer(loader); }, [loader]);}
import * as React from "react";
// ---------------------------------------------------------------------------
// The seam between the artifact page and the MCP-Apps shell that renders it.
//
// The shell (`@executor-js/mcp-apps-shell`) compiles model-written JSX and runs
// it inside a sandboxed iframe. It cannot be imported here: the shell already
// depends on THIS package for its shadcn component barrel, and turbo rejects
// the resulting package cycle outright (it errors, it does not warn). So the
// dependency is inverted the way the rest of the console does it — this package
// declares the contract, and the app composition roots (`apps/*`, which already
// depend on both) provide the implementation at startup.
//
// A host that never registers a renderer still gets a working artifacts list
// and a detail page that explains the artifact cannot be rendered here, rather
// than a crash — the same graceful-degradation rule the shared console follows
// for any host-specific capability.
//
// The seam carries a LOADER, not a component. The shell is browser-only by
// nature: it imports `@tailwindcss/browser`, which builds a `<style>` element
// at import scope, and it harvests `document.styleSheets` to seed the sandboxed
// iframe. Under SSR (cloud is TanStack Start) a static import of the shell puts
// that module in the server graph and every document request dies with
// `ReferenceError: document is not defined` before a component ever renders.
// Keeping the seam async means the composition roots hand over a `() =>
// import(...)` that only ever runs in the browser, so the shell's own code stays
// honestly browser-only instead of being polluted with `typeof document` guards.
// ---------------------------------------------------------------------------
/** What the page hands the shell: the stored source, and the live host. */
export interface ArtifactRendererProps {
/** The artifact's stored JSX source. */
readonly code: string;
/** Which artifact this is. The source addresses integrations by role, so the
* server needs the row to resolve a role to a connection; this is how the id
* reaches `execute-action`. */
readonly artifactId: string;
/** HTTP-backed MCP host — see `createHttpShellHost` in `./shell-host`. */
readonly host: unknown;
/**
* Called once, when the artifact has actually painted inside the frame.
*
* This is what lets the page hold ONE loading surface from navigation until
* there is something real to look at, rather than flashing a placeholder per
* stage of the boot. The shell owns the fact — the console cannot see inside
* a sandboxed frame two documents down — so it reports it; see
* `ARTIFACT_RENDERED_TOOL` in the shell package.
*
* Fires for the error surfaces too. "Rendered" here means presented, not
* succeeded: a compile failure is still the moment the user should be looking
* at the frame instead of a skeleton, and a signal that fired only on success
* would leave every failed artifact under a loading state forever.
*
* Optional, so a host that does not care simply omits it and nothing in the
* chain changes.
*/
readonly onRendered?: (() => void) | undefined;
}
export type ArtifactRenderer = React.ComponentType<ArtifactRendererProps>;
/**
* Resolves the renderer on demand, in the browser only. Apps pass a dynamic
* `import()`; the module is never touched during SSR.
*/
export type ArtifactRendererLoader = () => Promise<{ readonly default: ArtifactRenderer }>;
const ArtifactRendererContext = React.createContext<ArtifactRendererLoader | null>(null);
/**
* Provide the shell implementation. Apps mount this above the console shell:
*
* ```tsx
* <ArtifactRendererProvider
* loader={() => import("@executor-js/mcp-apps-shell/shell/artifact-renderer")}
* >
* ```
*
* The loader must resolve to a module whose `default` export is the renderer,
* which is what `React.lazy` consumes directly.
*/
export function ArtifactRendererProvider(
props: React.PropsWithChildren<{ readonly loader: ArtifactRendererLoader }>,
) {
return (
<ArtifactRendererContext.Provider value={props.loader}>
{props.children}
</ArtifactRendererContext.Provider>
);
}
/** The registered loader, or `null` on a host that provides none. */
export function useArtifactRendererLoader(): ArtifactRendererLoader | null {
return React.useContext(ArtifactRendererContext);
}
/**
* The lazy renderer component for the registered loader, or `null` when no host
* registered one.
*
* `React.lazy` is memoized per loader identity: calling it inside render would
* mint a fresh component type every pass and remount the shell (throwing away
* the compiled iframe) on every parent re-render.
*/
export function useArtifactRenderer(): ArtifactRenderer | null {
const loader = useArtifactRendererLoader();
return React.useMemo(() => (loader ? React.lazy(loader) : null), [loader]);
}
/**
* Loaders whose chunk has already been asked for.
*
* The dedupe is not for the network — a bundler's own module registry already
* serves a second `import()` of the same specifier from memory. It is so this
* module does not hand React's scheduler a new promise on every render of every
* component that preloads, and so the cost of "preload on hover" style callers
* is provably zero after the first.
*
* Keyed by loader identity, matching how `useArtifactRenderer` memoizes
* `React.lazy`; the composition roots define their loader at module scope
* precisely so that identity is stable for the life of the app.
*/
const preloaded = new WeakSet<ArtifactRendererLoader>();
/**
* Start fetching the renderer chunk before anything needs to render it.
*
* ## What this removes
*
* The renderer arrives through a dynamic `import()`, and until now that import
* was first touched when the detail page rendered — which is only after the
* artifact row has been fetched. So a cold open serialized two waits the user
* saw as two different placeholders: "loading the row", then "loading the code
* that draws rows". They are independent, and overlapping them makes the second
* free.
*
* ## Why fire-and-forget is right
*
* There is nothing to do with the result. Success populates the bundler's
* module cache, which is the entire point, and `React.lazy` will find it there.
* FAILURE is deliberately swallowed here rather than surfaced: this is a
* speculative fetch, the user may never open an artifact, and a chunk that
* genuinely cannot load must fail where a user is actually waiting on it — at
* the `Suspense` boundary, with the page's own error surface — not as an
* unhandled rejection logged from a hover. Retrying is automatic in the sense
* that matters: `React.lazy` calls the same loader again and gets a fresh
* attempt, because a rejected `import()` is not cached by any bundler.
*/
export function preloadArtifactRenderer(loader: ArtifactRendererLoader | null): void {
if (!loader || preloaded.has(loader)) return;
preloaded.add(loader);
void loader().then(
() => undefined,
() => {
// Let a failed speculative fetch be tried again for real. Without this a
// transient network blip during the preload would mark the loader done
// and the actual open would never re-attempt from here.
preloaded.delete(loader);
},
);
}
/**
* Kick the renderer chunk as soon as the mounting surface exists.
*
* Mounted by the artifacts LIST as well as the detail page: by the time a user
* has the gallery on screen, opening one of the cards is the only thing that
* page is for, so the chunk is fetched during the time they spend choosing.
* That is what turns the renderer's own placeholder into something that never
* appears in a warm session, and shortens it to nothing in a cold one.
*
* An effect rather than a render-phase call, so the fetch is never started
* during SSR or a discarded render pass.
*/
export function usePreloadArtifactRenderer(): void {
const loader = useArtifactRendererLoader();
React.useEffect(() => {
preloadArtifactRenderer(loader);
}, [loader]);
}