"use client"; import * as React from "react"; import { connectGuestBridge, type GuestBridge, type SandboxContext, type ThemeTokens, } from "@colineapp/app-runtime"; import type { ColineCapabilities } from "@colineapp/sdk/v2"; /** * The app-side runtime context (plan §7.2.4, §7.2.6): connects the guest * bridge, applies host theme tokens to the frame's root element (dark * mode flips live), and exposes `coline`, the surface context, and * data-fetching hooks to app components. */ interface ColineRuntime { bridge: GuestBridge; coline: ColineCapabilities; context: SandboxContext; colorScheme: "light" | "dark"; } const ColineRuntimeContext = React.createContext(null); function applyTheme(theme: ThemeTokens, colorScheme: "light" | "dark"): void { const root = document.documentElement; for (const [token, value] of Object.entries(theme)) { if (token.startsWith("--")) { root.style.setProperty(token, value); } } root.classList.toggle("dark", colorScheme === "dark"); } export function ColineAppProvider({ children }: { children: React.ReactNode }) { const [connectionError, setConnectionError] = React.useState(null); const [runtime, setRuntime] = React.useState(null); React.useEffect(() => { let disposed = false; let activeBridge: GuestBridge | null = null; let removeThemeListener: (() => void) | null = null; void connectGuestBridge() .then((bridge) => { if (disposed) { bridge.dispose(); return; } activeBridge = bridge; applyTheme(bridge.theme, bridge.colorScheme); removeThemeListener = bridge.onThemeChange((theme, colorScheme) => { applyTheme(theme, colorScheme); setRuntime((current) => (current ? { ...current, colorScheme } : current)); }); setRuntime({ bridge, coline: bridge.coline, context: bridge.context, colorScheme: bridge.colorScheme, }); }) .catch((error: unknown) => { if (!disposed) setConnectionError(error instanceof Error ? error.message : String(error)); }); return () => { disposed = true; activeBridge?.dispose(); removeThemeListener?.(); }; }, []); React.useEffect(() => { if (!runtime) return; // Keep the host informed of content height for inline embeds. const observer = new ResizeObserver(() => { runtime.bridge.reportHeight(document.documentElement.scrollHeight); }); observer.observe(document.documentElement); return () => observer.disconnect(); }, [runtime]); if (!runtime) { return (

{connectionError ?? "Connecting to Coline…"}

); } return {children}; } export function useColine(): ColineCapabilities { const runtime = React.useContext(ColineRuntimeContext); if (!runtime) { throw new Error("useColine must be used inside ."); } return runtime.coline; } export function useColineContext(): SandboxContext { const runtime = React.useContext(ColineRuntimeContext); if (!runtime) { throw new Error("useColineContext must be used inside ."); } return runtime.context; } export function useColorScheme(): "light" | "dark" { const runtime = React.useContext(ColineRuntimeContext); if (!runtime) { throw new Error("useColorScheme must be used inside ."); } return runtime.colorScheme; } export interface ColineQueryState { data: T | null; error: string | null; isLoading: boolean; refetch: () => void; } /** * Minimal async-state hook over a capability call (plan §7.2.6). Not a * cache — apps needing one can bring their own; this covers the common * load-render-refresh loop without dependencies. */ export function useColineQuery( loader: (coline: ColineCapabilities) => Promise, deps: React.DependencyList = [], ): ColineQueryState { const coline = useColine(); const [state, setState] = React.useState<{ data: T | null; error: string | null; isLoading: boolean; }>({ data: null, error: null, isLoading: true }); const [nonce, setNonce] = React.useState(0); const loaderRef = React.useRef(loader); loaderRef.current = loader; React.useEffect(() => { let cancelled = false; setState((current) => ({ ...current, isLoading: true, error: null })); void loaderRef .current(coline) .then((data) => { if (!cancelled) setState({ data, error: null, isLoading: false }); }) .catch((error: unknown) => { if (!cancelled) { setState({ data: null, error: error instanceof Error ? error.message : String(error), isLoading: false, }); } }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [coline, nonce, ...deps]); const refetch = React.useCallback(() => setNonce((value) => value + 1), []); return { ...state, refetch }; }