'use client'; // Host-rendered modal for plugin-requested confirm/alert/prompt dialogs. // Subscribes to the host-dialog queue and renders the head request, one at // a time. Closing the modal advances the queue. Prompts collect one or more // (optionally masked) fields so plugins never fall back to `window.prompt`, // which the sandbox blocks. import React, { useEffect, useMemo, useState, useSyncExternalStore } from 'react'; import { head, resolveHead, subscribe } from '@/lib/plugin-sandbox/host-dialog'; // Lightweight **bold** support in plugin dialog messages. Everything else is // rendered literally (newlines come from the parent's white-space: pre-wrap). // Splitting on the ** delimiter yields alternating plain/bold segments (odd // indices are bold). Plugins control these strings, so the delimiters balance. function renderMessage(message?: string): React.ReactNode { if (!message) return null; return message.split('**').map((seg, i) => i % 2 === 1 ? {seg} : {seg}, ); } export function PluginDialogHost(): React.JSX.Element | null { const current = useSyncExternalStore(subscribe, head, () => null); const isPrompt = current?.kind === 'prompt'; const fields = useMemo(() => (isPrompt ? current?.fields ?? [] : []), [isPrompt, current]); // Field values for a prompt, re-initialised whenever a new dialog surfaces. const [values, setValues] = useState>({}); useEffect(() => { if (!current) return; const init: Record = {}; for (const f of current.fields ?? []) init[f.name] = ''; setValues(init); }, [current]); const canSubmit = fields.every((f) => !f.required || (values[f.name] ?? '').length > 0); const cancel = () => resolveHead(current?.kind === 'prompt' ? null : false); const submitPrompt = () => { if (canSubmit) resolveHead(values); }; useEffect(() => { if (!current) return; function onKey(e: KeyboardEvent) { if (e.key === 'Escape') { e.preventDefault(); cancel(); } else if (e.key === 'Enter' && !isPrompt) { // For prompts, Enter is handled by the form (so it respects required // validation and works from within an input); non-prompt dialogs accept. e.preventDefault(); resolveHead(true); } } document.addEventListener('keydown', onKey, true); return () => document.removeEventListener('keydown', onKey, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [current, isPrompt, canSubmit, values]); if (!current) return null; const confirmLabel = current.confirmLabel ?? (current.kind === 'alert' ? 'OK' : current.kind === 'prompt' ? 'Submit' : 'Confirm'); const cancelLabel = current.cancelLabel ?? 'Cancel'; const btnBase: React.CSSProperties = { padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 500, cursor: 'pointer' }; const secondaryBtn: React.CSSProperties = { ...btnBase, border: '1px solid var(--color-border, #e2e8f0)', background: 'transparent', color: 'inherit' }; const primaryBtn: React.CSSProperties = { ...btnBase, border: '1px solid transparent', background: 'var(--color-primary, #3b82f6)', color: 'var(--color-primary-foreground, #fff)' }; return (
{ if (e.target === e.currentTarget) cancel(); }} >

{current.title}

{current.message && (

{renderMessage(current.message)}

)} {isPrompt ? (
{ e.preventDefault(); submitPrompt(); }}>
{fields.map((f, i) => ( ))}
) : (
{current.kind === 'confirm' && ( )}
)}
From plugin: {current.pluginId}
); }