Input: {"content":"\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n  DocumentEditorHostProps,\n  LeptosTiptapHostBridgeState,\n} from \"@/components/editor/editor-host-types\";\nimport { blocksFromTiptapDoc, tiptapDocFromBlocks } from \"@/lib/documents/tiptap-content-converter\";\nimport { useEditorBridgeStore } from \"@/store/editor-bridge\";\nimport { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";\nimport type { EditorReferenceBridge } from \"@/store/editor-bridge\";\nimport type { DocumentStats } from \"@/types/page-options\";\nimport type { Json } from \"@/types/supabase\";\n\nconst RUNTIME_PORT = 8123;\nconst RUNTIME_NAME = \"8123-leptos-tiptap-runtime\";\nconst RUNTIME_VERSION = \"1.1.0\";\nconst EVENT_PREFIX = \"mnote:leptos-tiptap-spike\";\nconst READY_EVENT = `${EVENT_PREFIX}:ready`;\nconst CHANGE_EVENT = `${EVENT_PREFIX}:change`;\nconst STATE_EVENT = `${EVENT_PREFIX}:state`;\nconst SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;\nconst HEIGHT_EVENT = `${EVENT_PREFIX}:height`;\nconst BOOTSTRAP_EVENT = `${EVENT_PREFIX}:bootstrap`;\nconst REPLACE_DOCUMENT_EVENT = `${EVENT_PREFIX}:replace-document`;\nconst PROTOCOL = \"mnote.leptos_tiptap.bridge.v1\";\nconst EMPTY_DOC = { type: \"doc\", content: [{ type: \"paragraph\", attrs: {}, content: [] }] } as const;\nconst FALLBACK_IFRAME_MIN_HEIGHT = 720;\n\ntype HostEnvelope<T = unknown> = {\n  protocol?: string;\n  runtime?: string;\n  version?: string;\n  source?: string;\n  event?: string;\n  payload?: T;\n};\n\ntype HostDocumentPayload = {\n  title?: string | null;\n  content?: unknown;\n  meta?: {\n    dirty_count?: number;\n    editor_focused?: boolean;\n    slash_open?: boolean;\n    toolbar_open?: boolean;\n    revision?: number | null;\n    conflict_detection_key?: string | null;\n  };\n};\n\ntype HeightPayload = {\n  height?: number;\n};\n\ntype BootstrapPayload = {\n  documentId: string;\n  workspaceId: string;\n  title: string | null;\n  content: unknown;\n  revision: number | null;\n  conflictDetectionKey: string | null;\n  readOnly: boolean;\n};\n\nfunction resolveRuntimeUrl() {\n  if (typeof window === \"undefined\") {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n  const cfg = getMnoteRuntimeConfig();\n  if (cfg.mnoteWebBaseUrl) {\n    try {\n      const url = new URL(cfg.mnoteWebBaseUrl);\n      url.port = String(RUNTIME_PORT);\n      url.pathname = \"/\";\n      url.search = \"\";\n      url.hash = \"\";\n      return url.toString();\n    } catch {\n      // ignore\n    }\n  }\n  try {\n    const url = new URL(window.location.href);\n    url.port = String(RUNTIME_PORT);\n    url.pathname = \"/\";\n    url.search = \"\";\n    url.hash = \"\";\n    return url.toString();\n  } catch {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n}\n\nfunction normalizeRuntimeDoc(tiptapDoc: Json) {\n  if (!tiptapDoc || typeof tiptapDoc !== \"object\") {\n    return EMPTY_DOC;\n  }\n  const doc = tiptapDoc as { type?: unknown; content?: unknown };\n  if (doc.type !== \"doc\") {\n    return EMPTY_DOC;\n  }\n  if (!Array.isArray(doc.content) || doc.content.length === 0) {\n    return EMPTY_DOC;\n  }\n  return tiptapDoc;\n}\n\nfunction buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, reloadKey: number) {\n  const url = new URL(runtimeUrl);\n  url.searchParams.set(\"embedded\", \"1\");\n  url.searchParams.set(\"host\", \"leptos_tiptap\");\n  url.searchParams.set(\"documentId\", props.documentId);\n  url.searchParams.set(\"workspaceId\", props.workspaceId);\n  url.searchParams.set(\"reloadKey\", String(reloadKey));\n  return url.toString();\n}\n\nfunction isBridgeEnvelope(value: unknown): value is HostEnvelope {\n  if (!value || typeof value !== \"object\") {\n    return false;\n  }\n  const maybe = value as HostEnvelope;\n  return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;\n}\n\nfunction flattenText(value: unknown): string {\n  if (typeof value === \"string\") return value;\n  if (Array.isArray(value)) return value.map(flattenText).join(\"\");\n  if (value && typeof value === \"object\") {\n    const obj = value as { text?: unknown; content?: unknown };\n    return `${flattenText(obj.text)}${flattenText(obj.content)}`;\n  }\n  return \"\";\n}\n\nfunction buildStats(blocks: Json): DocumentStats {\n  const items = Array.isArray(blocks) ? blocks : [];\n  const text = items\n    .map((item) => (item && typeof item === \"object\" ? flattenText((item as { content?: unknown }).content) : \"\"))\n    .join(\"\\n\")\n    .trim();\n  const todoItems = items.filter((item) => item && typeof item === \"object\" && (item as { type?: unknown }).type === \"todo\");\n  const todoDone = todoItems.filter(\n    (item) => item && typeof item === \"object\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),\n  ).length;\n  return {\n    wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,\n    characterCount: text.length,\n    blockCount: items.length,\n    todoTotal: todoItems.length,\n    todoDone,\n  };\n}\n\nfunction toIsoNow() {\n  return new Date().toISOString();\n}\n\nfunction coerceHeight(value: unknown): number {\n  const height = Number(value);\n  if (!Number.isFinite(height)) {\n    return FALLBACK_IFRAME_MIN_HEIGHT;\n  }\n  return Math.max(FALLBACK_IFRAME_MIN_HEIGHT, Math.ceil(height));\n}\n\nfunction postBridgeMessage(targetWindow: Window | null | undefined, event: string, payload: unknown) {\n  if (!targetWindow) return;\n  const envelope: HostEnvelope = {\n    protocol: PROTOCOL,\n    runtime: RUNTIME_NAME,\n    version: RUNTIME_VERSION,\n    source: EVENT_PREFIX,\n    event,\n    payload,\n  };\n  targetWindow.postMessage(envelope, \"*\");\n}\n\nexport function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {\n  const iframeRef = useRef<HTMLIFrameElement | null>(null);\n  const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);\n  const registerBridge = useEditorBridgeStore((state) => state.registerBridge);\n  const onSnapshotRef = useRef(props.onSnapshot);\n  const onStatsChangeRef = useRef(props.onStatsChange);\n  const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);\n  const revisionRef = useRef<number | null>(props.initialRevision ?? null);\n  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);\n  const bootstrapPayloadRef = useRef<BootstrapPayload | null>(null);\n  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => tiptapDocFromBlocks(props.initialContent as Json) as Json);\n  const [reloadKey, setReloadKey] = useState(0);\n  const [iframeHeight, setIframeHeight] = useState(FALLBACK_IFRAME_MIN_HEIGHT);\n  const [bridgeState, setBridgeState] = useState<LeptosTiptapHostBridgeState>({\n    ready: false,\n    runtimeName: RUNTIME_NAME,\n    runtimeVersion: RUNTIME_VERSION,\n    runtimeUrl,\n    documentId: props.documentId,\n    workspaceId: props.workspaceId,\n    status: \"booting\",\n    lastChangeAt: null,\n    lastSaveRequestAt: null,\n    lastError: null,\n  });\n\n  const iframeSrc = useMemo(\n    () => buildIframeSrc(runtimeUrl, props, reloadKey),\n    [props, reloadKey, runtimeUrl],\n  );\n\n  useEffect(() => {\n    onSnapshotRef.current = props.onSnapshot;\n    onStatsChangeRef.current = props.onStatsChange;\n    onPersistedMetaChangeRef.current = props.onPersistedMetaChange;\n    revisionRef.current = props.initialRevision ?? null;\n    conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;\n  }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);\n\n  useEffect(() => {\n    const nextRuntimeDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;\n    setRuntimeDoc(nextRuntimeDoc);\n    bootstrapPayloadRef.current = {\n      documentId: props.documentId,\n      workspaceId: props.workspaceId,\n      title: props.title ?? null,\n      content: normalizeRuntimeDoc(nextRuntimeDoc),\n      revision: props.initialRevision ?? null,\n      conflictDetectionKey: props.initialConflictDetectionKey ?? null,\n      readOnly: Boolean(props.readOnly),\n    };\n    setIframeHeight(FALLBACK_IFRAME_MIN_HEIGHT);\n    setReloadKey((value) => value + 1);\n    setBridgeState((prev) => ({\n      ...prev,\n      ready: false,\n      documentId: props.documentId,\n      workspaceId: props.workspaceId,\n      status: \"booting\",\n      lastError: null,\n    }));\n  }, [props.documentId, props.initialContent, props.initialConflictDetectionKey, props.initialRevision, props.readOnly, props.title, props.workspaceId]);\n\n  useEffect(() => {\n    const iframeWindow = iframeRef.current?.contentWindow;\n    if (!iframeWindow) return;\n    const payload = bootstrapPayloadRef.current;\n    if (!payload) return;\n    postBridgeMessage(iframeWindow, REPLACE_DOCUMENT_EVENT, payload);\n    setBridgeState((prev) => ({ ...prev, status: prev.ready ? \"reloading\" : prev.status }));\n  }, [runtimeDoc]);\n\n  useEffect(() => {\n    const bridge: EditorReferenceBridge = {\n      insertInlineReference: () => ({ blockId: null }),\n      insertEmbedReference: () => ({ blockId: null }),\n      undo: () => {\n        // 现阶段 iframe runtime 还没有父->子撤销命令，先保持最小 bridge 兼容。\n      },\n      redo: () => {\n        // 现阶段 iframe runtime 还没有父->子重做命令，先保持最小 bridge 兼容。\n      },\n      getCursorBlockId: () => null,\n      replaceWithSnapshot: (blocks: Json) => {\n        const nextRuntimeDoc = tiptapDocFromBlocks(blocks) as Json;\n        setRuntimeDoc(nextRuntimeDoc);\n        bootstrapPayloadRef.current = {\n          documentId: props.documentId,\n          workspaceId: props.workspaceId,\n          title: props.title ?? null,\n          content: normalizeRuntimeDoc(nextRuntimeDoc),\n          revision: revisionRef.current,\n          conflictDetectionKey: conflictDetectionKeyRef.current,\n          readOnly: Boolean(props.readOnly),\n        };\n        postBridgeMessage(iframeRef.current?.contentWindow, REPLACE_DOCUMENT_EVENT, bootstrapPayloadRef.current);\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: false,\n          status: \"reloading\",\n          lastError: null,\n        }));\n      },\n    };\n    registerBridge(bridge);\n    return () => registerBridge(null);\n  }, [props.documentId, props.readOnly, props.title, props.workspaceId, registerBridge]);\n\n  useEffect(() => {\n    let disposed = false;\n    const expectedOrigin = (() => {\n      try {\n        return new URL(runtimeUrl).origin;\n      } catch {\n        return \"\";\n      }\n    })();\n\n    const publishSnapshot = (payload: HostDocumentPayload) => {\n      const blocks = blocksFromTiptapDoc(payload.content) as Json;\n      const stats = buildStats(blocks);\n      setRuntimeDoc(normalizeRuntimeDoc(payload.content as Json) as Json);\n      onSnapshotRef.current?.({ blocks, stats });\n      onStatsChangeRef.current?.(stats);\n      return { blocks, stats };\n    };\n\n    const saveSnapshot = async (payload: HostDocumentPayload) => {\n      const { blocks } = publishSnapshot(payload);\n      const response = await fetch(\"/api/documents/save\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          documentId: props.documentId,\n          workspaceId: props.workspaceId,\n          revision: payload.meta?.revision ?? revisionRef.current,\n          conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,\n          snapshotCapturedAt: toIsoNow(),\n          blockCount: Array.isArray(blocks) ? blocks.length : 0,\n          content: blocks,\n        }),\n      });\n      const nextMeta = await response.json().catch(() => null);\n      if (!response.ok) {\n        const errorMessage = typeof nextMeta?.error === \"string\" ? nextMeta.error : `保存失败（${response.status}）`;\n        throw new Error(errorMessage);\n      }\n      const nextRevision = typeof nextMeta?.revision === \"number\" ? nextMeta.revision : payload.meta?.revision ?? null;\n      const nextConflictDetectionKey =\n        typeof nextMeta?.conflictDetectionKey === \"string\"\n          ? nextMeta.conflictDetectionKey\n          : payload.meta?.conflict_detection_key ?? null;\n      revisionRef.current = nextRevision;\n      conflictDetectionKeyRef.current = nextConflictDetectionKey;\n      onPersistedMetaChangeRef.current?.({\n        revision: nextRevision,\n        conflictDetectionKey: nextConflictDetectionKey,\n      });\n    };\n\n    const handleMessage = (event: MessageEvent<HostEnvelope>) => {\n      if (!expectedOrigin || event.origin !== expectedOrigin) {\n        return;\n      }\n      if (event.source !== iframeRef.current?.contentWindow) {\n        return;\n      }\n      if (!isBridgeEnvelope(event.data)) {\n        return;\n      }\n      const eventName = typeof event.data.event === \"string\" ? event.data.event : \"\";\n      const payload = (event.data.payload ?? {}) as HostDocumentPayload & HeightPayload;\n      if (eventName === READY_EVENT) {\n        const bootstrapPayload = bootstrapPayloadRef.current;\n        if (bootstrapPayload) {\n          postBridgeMessage(iframeRef.current?.contentWindow, BOOTSTRAP_EVENT, bootstrapPayload);\n        }\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"ready\", lastError: null }));\n        return;\n      }\n      if (eventName === STATE_EVENT) {\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"editing\", lastError: null }));\n        return;\n      }\n      if (eventName === HEIGHT_EVENT) {\n        setIframeHeight(coerceHeight(payload.height));\n        return;\n      }\n      if (eventName === CHANGE_EVENT) {\n        publishSnapshot(payload);\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"dirty\", lastChangeAt: toIsoNow(), lastError: null }));\n        return;\n      }\n      if (eventName === SAVE_REQUEST_EVENT) {\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: true,\n          status: \"saving\",\n          lastSaveRequestAt: toIsoNow(),\n          lastError: null,\n        }));\n        void saveSnapshot(payload)\n          .then(() => {\n            if (disposed) return;\n            setBridgeState((prev) => ({ ...prev, ready: true, status: \"saved\", lastError: null }));\n          })\n          .catch((error) => {\n            if (disposed) return;\n            setBridgeState((prev) => ({\n              ...prev,\n              ready: true,\n              status: \"error\",\n              lastError: error instanceof Error ? error.message : \"保存失败\",\n            }));\n          });\n      }\n    };\n\n    window.addEventListener(\"message\", handleMessage);\n    return () => {\n      disposed = true;\n      window.removeEventListener(\"message\", handleMessage);\n    };\n  }, [props.documentId, props.workspaceId, runtimeUrl]);\n\n  return (\n    <iframe\n      key={`${props.documentId}:${reloadKey}`}\n      ref={iframeRef}\n      src={iframeSrc}\n      title=\"Leptos Tiptap Editor\"\n      className=\"w-full border-0\"\n      style={{ height: `${iframeHeight}px`, minHeight: `${FALLBACK_IFRAME_MIN_HEIGHT}px` }}\n      data-bridge-status={bridgeState.status}\n      data-runtime-name={bridgeState.runtimeName}\n      data-runtime-version={bridgeState.runtimeVersion}\n    />\n  );\n}\n","path":"/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx"}


Output:
{
  "type": "update",
  "filePath": "/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-editor-host.tsx",
  "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n  DocumentEditorHostProps,\n  LeptosTiptapHostBridgeState,\n} from \"@/components/editor/editor-host-types\";\nimport { blocksFromTiptapDoc, tiptapDocFromBlocks } from \"@/lib/documents/tiptap-content-converter\";\nimport { useEditorBridgeStore } from \"@/store/editor-bridge\";\nimport { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";\nimport type { EditorReferenceBridge } from \"@/store/editor-bridge\";\nimport type { DocumentStats } from \"@/types/page-options\";\nimport type { Json } from \"@/types/supabase\";\n\nconst RUNTIME_PORT = 8123;\nconst RUNTIME_NAME = \"8123-leptos-tiptap-runtime\";\nconst RUNTIME_VERSION = \"1.1.0\";\nconst EVENT_PREFIX = \"mnote:leptos-tiptap-spike\";\nconst READY_EVENT = `${EVENT_PREFIX}:ready`;\nconst CHANGE_EVENT = `${EVENT_PREFIX}:change`;\nconst STATE_EVENT = `${EVENT_PREFIX}:state`;\nconst SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;\nconst HEIGHT_EVENT = `${EVENT_PREFIX}:height`;\nconst BOOTSTRAP_EVENT = `${EVENT_PREFIX}:bootstrap`;\nconst REPLACE_DOCUMENT_EVENT = `${EVENT_PREFIX}:replace-document`;\nconst PROTOCOL = \"mnote.leptos_tiptap.bridge.v1\";\nconst EMPTY_DOC = { type: \"doc\", content: [{ type: \"paragraph\", attrs: {}, content: [] }] } as const;\nconst FALLBACK_IFRAME_MIN_HEIGHT = 720;\n\ntype HostEnvelope<T = unknown> = {\n  protocol?: string;\n  runtime?: string;\n  version?: string;\n  source?: string;\n  event?: string;\n  payload?: T;\n};\n\ntype HostDocumentPayload = {\n  title?: string | null;\n  content?: unknown;\n  meta?: {\n    dirty_count?: number;\n    editor_focused?: boolean;\n    slash_open?: boolean;\n    toolbar_open?: boolean;\n    revision?: number | null;\n    conflict_detection_key?: string | null;\n  };\n};\n\ntype HeightPayload = {\n  height?: number;\n};\n\ntype BootstrapPayload = {\n  documentId: string;\n  workspaceId: string;\n  title: string | null;\n  content: unknown;\n  revision: number | null;\n  conflictDetectionKey: string | null;\n  readOnly: boolean;\n};\n\nfunction resolveRuntimeUrl() {\n  if (typeof window === \"undefined\") {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n  const cfg = getMnoteRuntimeConfig();\n  if (cfg.mnoteWebBaseUrl) {\n    try {\n      const url = new URL(cfg.mnoteWebBaseUrl);\n      url.port = String(RUNTIME_PORT);\n      url.pathname = \"/\";\n      url.search = \"\";\n      url.hash = \"\";\n      return url.toString();\n    } catch {\n      // ignore\n    }\n  }\n  try {\n    const url = new URL(window.location.href);\n    url.port = String(RUNTIME_PORT);\n    url.pathname = \"/\";\n    url.search = \"\";\n    url.hash = \"\";\n    return url.toString();\n  } catch {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n}\n\nfunction normalizeRuntimeDoc(tiptapDoc: Json) {\n  if (!tiptapDoc || typeof tiptapDoc !== \"object\") {\n    return EMPTY_DOC;\n  }\n  const doc = tiptapDoc as { type?: unknown; content?: unknown };\n  if (doc.type !== \"doc\") {\n    return EMPTY_DOC;\n  }\n  if (!Array.isArray(doc.content) || doc.content.length === 0) {\n    return EMPTY_DOC;\n  }\n  return tiptapDoc;\n}\n\nfunction buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, reloadKey: number) {\n  const url = new URL(runtimeUrl);\n  url.searchParams.set(\"embedded\", \"1\");\n  url.searchParams.set(\"host\", \"leptos_tiptap\");\n  url.searchParams.set(\"documentId\", props.documentId);\n  url.searchParams.set(\"workspaceId\", props.workspaceId);\n  url.searchParams.set(\"reloadKey\", String(reloadKey));\n  return url.toString();\n}\n\nfunction isBridgeEnvelope(value: unknown): value is HostEnvelope {\n  if (!value || typeof value !== \"object\") {\n    return false;\n  }\n  const maybe = value as HostEnvelope;\n  return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;\n}\n\nfunction flattenText(value: unknown): string {\n  if (typeof value === \"string\") return value;\n  if (Array.isArray(value)) return value.map(flattenText).join(\"\");\n  if (value && typeof value === \"object\") {\n    const obj = value as { text?: unknown; content?: unknown };\n    return `${flattenText(obj.text)}${flattenText(obj.content)}`;\n  }\n  return \"\";\n}\n\nfunction buildStats(blocks: Json): DocumentStats {\n  const items = Array.isArray(blocks) ? blocks : [];\n  const text = items\n    .map((item) => (item && typeof item === \"object\" ? flattenText((item as { content?: unknown }).content) : \"\"))\n    .join(\"\\n\")\n    .trim();\n  const todoItems = items.filter((item) => item && typeof item === \"object\" && (item as { type?: unknown }).type === \"todo\");\n  const todoDone = todoItems.filter(\n    (item) => item && typeof item === \"object\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),\n  ).length;\n  return {\n    wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,\n    characterCount: text.length,\n    blockCount: items.length,\n    todoTotal: todoItems.length,\n    todoDone,\n  };\n}\n\nfunction toIsoNow() {\n  return new Date().toISOString();\n}\n\nfunction coerceHeight(value: unknown): number {\n  const height = Number(value);\n  if (!Number.isFinite(height)) {\n    return FALLBACK_IFRAME_MIN_HEIGHT;\n  }\n  return Math.max(FALLBACK_IFRAME_MIN_HEIGHT, Math.ceil(height));\n}\n\nfunction postBridgeMessage(targetWindow: Window | null | undefined, event: string, payload: unknown) {\n  if (!targetWindow) return;\n  const envelope: HostEnvelope = {\n    protocol: PROTOCOL,\n    runtime: RUNTIME_NAME,\n    version: RUNTIME_VERSION,\n    source: EVENT_PREFIX,\n    event,\n    payload,\n  };\n  targetWindow.postMessage(envelope, \"*\");\n}\n\nexport function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {\n  const iframeRef = useRef<HTMLIFrameElement | null>(null);\n  const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);\n  const registerBridge = useEditorBridgeStore((state) => state.registerBridge);\n  const onSnapshotRef = useRef(props.onSnapshot);\n  const onStatsChangeRef = useRef(props.onStatsChange);\n  const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);\n  const revisionRef = useRef<number | null>(props.initialRevision ?? null);\n  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);\n  const bootstrapPayloadRef = useRef<BootstrapPayload | null>(null);\n  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => tiptapDocFromBlocks(props.initialContent as Json) as Json);\n  const [reloadKey, setReloadKey] = useState(0);\n  const [iframeHeight, setIframeHeight] = useState(FALLBACK_IFRAME_MIN_HEIGHT);\n  const [bridgeState, setBridgeState] = useState<LeptosTiptapHostBridgeState>({\n    ready: false,\n    runtimeName: RUNTIME_NAME,\n    runtimeVersion: RUNTIME_VERSION,\n    runtimeUrl,\n    documentId: props.documentId,\n    workspaceId: props.workspaceId,\n    status: \"booting\",\n    lastChangeAt: null,\n    lastSaveRequestAt: null,\n    lastError: null,\n  });\n\n  const iframeSrc = useMemo(\n    () => buildIframeSrc(runtimeUrl, props, reloadKey),\n    [props, reloadKey, runtimeUrl],\n  );\n\n  useEffect(() => {\n    onSnapshotRef.current = props.onSnapshot;\n    onStatsChangeRef.current = props.onStatsChange;\n    onPersistedMetaChangeRef.current = props.onPersistedMetaChange;\n    revisionRef.current = props.initialRevision ?? null;\n    conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;\n  }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);\n\n  useEffect(() => {\n    const nextRuntimeDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;\n    setRuntimeDoc(nextRuntimeDoc);\n    bootstrapPayloadRef.current = {\n      documentId: props.documentId,\n      workspaceId: props.workspaceId,\n      title: props.title ?? null,\n      content: normalizeRuntimeDoc(nextRuntimeDoc),\n      revision: props.initialRevision ?? null,\n      conflictDetectionKey: props.initialConflictDetectionKey ?? null,\n      readOnly: Boolean(props.readOnly),\n    };\n    setIframeHeight(FALLBACK_IFRAME_MIN_HEIGHT);\n    setReloadKey((value) => value + 1);\n    setBridgeState((prev) => ({\n      ...prev,\n      ready: false,\n      documentId: props.documentId,\n      workspaceId: props.workspaceId,\n      status: \"booting\",\n      lastError: null,\n    }));\n  }, [props.documentId, props.initialContent, props.initialConflictDetectionKey, props.initialRevision, props.readOnly, props.title, props.workspaceId]);\n\n  useEffect(() => {\n    const iframeWindow = iframeRef.current?.contentWindow;\n    if (!iframeWindow) return;\n    const payload = bootstrapPayloadRef.current;\n    if (!payload) return;\n    postBridgeMessage(iframeWindow, REPLACE_DOCUMENT_EVENT, payload);\n    setBridgeState((prev) => ({ ...prev, status: prev.ready ? \"reloading\" : prev.status }));\n  }, [runtimeDoc]);\n\n  useEffect(() => {\n    const bridge: EditorReferenceBridge = {\n      insertInlineReference: () => ({ blockId: null }),\n      insertEmbedReference: () => ({ blockId: null }),\n      undo: () => {\n        // 现阶段 iframe runtime 还没有父->子撤销命令，先保持最小 bridge 兼容。\n      },\n      redo: () => {\n        // 现阶段 iframe runtime 还没有父->子重做命令，先保持最小 bridge 兼容。\n      },\n      getCursorBlockId: () => null,\n      replaceWithSnapshot: (blocks: Json) => {\n        const nextRuntimeDoc = tiptapDocFromBlocks(blocks) as Json;\n        setRuntimeDoc(nextRuntimeDoc);\n        bootstrapPayloadRef.current = {\n          documentId: props.documentId,\n          workspaceId: props.workspaceId,\n          title: props.title ?? null,\n          content: normalizeRuntimeDoc(nextRuntimeDoc),\n          revision: revisionRef.current,\n          conflictDetectionKey: conflictDetectionKeyRef.current,\n          readOnly: Boolean(props.readOnly),\n        };\n        postBridgeMessage(iframeRef.current?.contentWindow, REPLACE_DOCUMENT_EVENT, bootstrapPayloadRef.current);\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: false,\n          status: \"reloading\",\n          lastError: null,\n        }));\n      },\n    };\n    registerBridge(bridge);\n    return () => registerBridge(null);\n  }, [props.documentId, props.readOnly, props.title, props.workspaceId, registerBridge]);\n\n  useEffect(() => {\n    let disposed = false;\n    const expectedOrigin = (() => {\n      try {\n        return new URL(runtimeUrl).origin;\n      } catch {\n        return \"\";\n      }\n    })();\n\n    const publishSnapshot = (payload: HostDocumentPayload) => {\n      const blocks = blocksFromTiptapDoc(payload.content) as Json;\n      const stats = buildStats(blocks);\n      setRuntimeDoc(normalizeRuntimeDoc(payload.content as Json) as Json);\n      onSnapshotRef.current?.({ blocks, stats });\n      onStatsChangeRef.current?.(stats);\n      return { blocks, stats };\n    };\n\n    const saveSnapshot = async (payload: HostDocumentPayload) => {\n      const { blocks } = publishSnapshot(payload);\n      const response = await fetch(\"/api/documents/save\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          documentId: props.documentId,\n          workspaceId: props.workspaceId,\n          revision: payload.meta?.revision ?? revisionRef.current,\n          conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,\n          snapshotCapturedAt: toIsoNow(),\n          blockCount: Array.isArray(blocks) ? blocks.length : 0,\n          content: blocks,\n        }),\n      });\n      const nextMeta = await response.json().catch(() => null);\n      if (!response.ok) {\n        const errorMessage = typeof nextMeta?.error === \"string\" ? nextMeta.error : `保存失败（${response.status}）`;\n        throw new Error(errorMessage);\n      }\n      const nextRevision = typeof nextMeta?.revision === \"number\" ? nextMeta.revision : payload.meta?.revision ?? null;\n      const nextConflictDetectionKey =\n        typeof nextMeta?.conflictDetectionKey === \"string\"\n          ? nextMeta.conflictDetectionKey\n          : payload.meta?.conflict_detection_key ?? null;\n      revisionRef.current = nextRevision;\n      conflictDetectionKeyRef.current = nextConflictDetectionKey;\n      onPersistedMetaChangeRef.current?.({\n        revision: nextRevision,\n        conflictDetectionKey: nextConflictDetectionKey,\n      });\n    };\n\n    const handleMessage = (event: MessageEvent<HostEnvelope>) => {\n      if (!expectedOrigin || event.origin !== expectedOrigin) {\n        return;\n      }\n      if (event.source !== iframeRef.current?.contentWindow) {\n        return;\n      }\n      if (!isBridgeEnvelope(event.data)) {\n        return;\n      }\n      const eventName = typeof event.data.event === \"string\" ? event.data.event : \"\";\n      const payload = (event.data.payload ?? {}) as HostDocumentPayload & HeightPayload;\n      if (eventName === READY_EVENT) {\n        const bootstrapPayload = bootstrapPayloadRef.current;\n        if (bootstrapPayload) {\n          postBridgeMessage(iframeRef.current?.contentWindow, BOOTSTRAP_EVENT, bootstrapPayload);\n        }\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"ready\", lastError: null }));\n        return;\n      }\n      if (eventName === STATE_EVENT) {\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"editing\", lastError: null }));\n        return;\n      }\n      if (eventName === HEIGHT_EVENT) {\n        setIframeHeight(coerceHeight(payload.height));\n        return;\n      }\n      if (eventName === CHANGE_EVENT) {\n        publishSnapshot(payload);\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"dirty\", lastChangeAt: toIsoNow(), lastError: null }));\n        return;\n      }\n      if (eventName === SAVE_REQUEST_EVENT) {\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: true,\n          status: \"saving\",\n          lastSaveRequestAt: toIsoNow(),\n          lastError: null,\n        }));\n        void saveSnapshot(payload)\n          .then(() => {\n            if (disposed) return;\n            setBridgeState((prev) => ({ ...prev, ready: true, status: \"saved\", lastError: null }));\n          })\n          .catch((error) => {\n            if (disposed) return;\n            setBridgeState((prev) => ({\n              ...prev,\n              ready: true,\n              status: \"error\",\n              lastError: error instanceof Error ? error.message : \"保存失败\",\n            }));\n          });\n      }\n    };\n\n    window.addEventListener(\"message\", handleMessage);\n    return () => {\n      disposed = true;\n      window.removeEventListener(\"message\", handleMessage);\n    };\n  }, [props.documentId, props.workspaceId, runtimeUrl]);\n\n  return (\n    <iframe\n      key={`${props.documentId}:${reloadKey}`}\n      ref={iframeRef}\n      src={iframeSrc}\n      title=\"Leptos Tiptap Editor\"\n      className=\"w-full border-0\"\n      style={{ height: `${iframeHeight}px`, minHeight: `${FALLBACK_IFRAME_MIN_HEIGHT}px` }}\n      data-bridge-status={bridgeState.status}\n      data-runtime-name={bridgeState.runtimeName}\n      data-runtime-version={bridgeState.runtimeVersion}\n    />\n  );\n}\n",
  "structuredPatch": [
    {
      "oldStart": 1,
      "oldLines": 349,
      "newStart": 1,
      "newLines": 419,
      "lines": [
        "-\"use client\";",
        "-",
        "-import { useEffect, useMemo, useRef, useState } from \"react\";",
        "-import type {",
        "-  DocumentEditorHostProps,",
        "-  LeptosTiptapHostBridgeState,",
        "-} from \"@/components/editor/editor-host-types\";",
        "-import { blocksFromTiptapDoc, tiptapDocFromBlocks } from \"@/lib/documents/tiptap-content-converter\";",
        "-import { useEditorBridgeStore } from \"@/store/editor-bridge\";",
        "-import { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";",
        "-import type { EditorReferenceBridge } from \"@/store/editor-bridge\";",
        "-import type { DocumentStats } from \"@/types/page-options\";",
        "-import type { Json } from \"@/types/supabase\";",
        "-",
        "-const RUNTIME_PORT = 8123;",
        "-const RUNTIME_NAME = \"8123-leptos-tiptap-runtime\";",
        "-const RUNTIME_VERSION = \"1.0.0\";",
        "-const EVENT_PREFIX = \"mnote:leptos-tiptap-spike\";",
        "-const READY_EVENT = `${EVENT_PREFIX}:ready`;",
        "-const CHANGE_EVENT = `${EVENT_PREFIX}:change`;",
        "-const STATE_EVENT = `${EVENT_PREFIX}:state`;",
        "-const SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;",
        "-const PROTOCOL = \"mnote.leptos_tiptap.bridge.v1\";",
        "-const EMPTY_DOC = { type: \"doc\", content: [{ type: \"paragraph\" }] } as const;",
        "-",
        "-type HostEnvelope<T = unknown> = {",
        "-  protocol?: string;",
        "-  runtime?: string;",
        "-  version?: string;",
        "-  source?: string;",
        "-  event?: string;",
        "-  payload?: T;",
        "-};",
        "-",
        "-type HostDocumentPayload = {",
        "-  title?: string;",
        "-  content?: unknown;",
        "-  meta?: {",
        "-    dirty_count?: number;",
        "-    editor_focused?: boolean;",
        "-    slash_open?: boolean;",
        "-    toolbar_open?: boolean;",
        "-    revision?: number | null;",
        "-    conflict_detection_key?: string | null;",
        "-  };",
        "-};",
        "-",
        "-function resolveRuntimeUrl() {",
        "-  if (typeof window === \"undefined\") {",
        "-    return `http://localhost:${RUNTIME_PORT}/`;",
        "-  }",
        "-  const cfg = getMnoteRuntimeConfig();",
        "-  if (cfg.mnoteWebBaseUrl) {",
        "-    try {",
        "-      const url = new URL(cfg.mnoteWebBaseUrl);",
        "-      url.port = String(RUNTIME_PORT);",
        "-      url.pathname = \"/\";",
        "-      url.search = \"\";",
        "-      url.hash = \"\";",
        "-      return url.toString();",
        "-    } catch {",
        "-      // ignore",
        "-    }",
        "-  }",
        "-  try {",
        "-    const url = new URL(window.location.href);",
        "-    url.port = String(RUNTIME_PORT);",
        "-    url.pathname = \"/\";",
        "-    url.search = \"\";",
        "-    url.hash = \"\";",
        "-    return url.toString();",
        "-  } catch {",
        "-    return `http://localhost:${RUNTIME_PORT}/`;",
        "-  }",
        "-}",
        "-",
        "-function normalizeRuntimeDoc(tiptapDoc: Json) {",
        "-  if (!tiptapDoc || typeof tiptapDoc !== \"object\") {",
        "-    return EMPTY_DOC;",
        "-  }",
        "-  const doc = tiptapDoc as { type?: unknown; content?: unknown };",
        "-  if (doc.type !== \"doc\") {",
        "-    return EMPTY_DOC;",
        "-  }",
        "-  if (!Array.isArray(doc.content) || doc.content.length === 0) {",
        "-    return EMPTY_DOC;",
        "-  }",
        "-  return tiptapDoc;",
        "-}",
        "-",
        "-function buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, tiptapDoc: Json, reloadKey: number) {",
        "-  const url = new URL(runtimeUrl);",
        "-  url.searchParams.set(\"embedded\", \"1\");",
        "-  url.searchParams.set(\"host\", \"leptos_tiptap\");",
        "-  url.searchParams.set(\"documentId\", props.documentId);",
        "-  url.searchParams.set(\"workspaceId\", props.workspaceId);",
        "-  url.searchParams.set(\"reloadKey\", String(reloadKey));",
        "-  url.searchParams.set(\"content\", JSON.stringify(normalizeRuntimeDoc(tiptapDoc)));",
        "-  if (props.initialRevision != null) {",
        "-    url.searchParams.set(\"revision\", String(props.initialRevision));",
        "-  }",
        "-  if (props.initialConflictDetectionKey) {",
        "-    url.searchParams.set(\"conflictDetectionKey\", props.initialConflictDetectionKey);",
        "-  }",
        "-  return url.toString();",
        "-}",
        "-",
        "-function isBridgeEnvelope(value: unknown): value is HostEnvelope {",
        "-  if (!value || typeof value !== \"object\") {",
        "-    return false;",
        "-  }",
        "-  const maybe = value as HostEnvelope;",
        "-  return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;",
        "-}",
        "-",
        "-function flattenText(value: unknown): string {",
        "-  if (typeof value === \"string\") return value;",
        "-  if (Array.isArray(value)) return value.map(flattenText).join(\"\");",
        "-  if (value && typeof value === \"object\") {",
        "-    const obj = value as { text?: unknown; content?: unknown };",
        "-    return `${flattenText(obj.text)}${flattenText(obj.content)}`;",
        "-  }",
        "-  return \"\";",
        "-}",
        "-",
        "-function buildStats(blocks: Json): DocumentStats {",
        "-  const items = Array.isArray(blocks) ? blocks : [];",
        "-  const text = items",
        "-    .map((item) => (item && typeof item === \"object\" ? flattenText((item as { content?: unknown }).content) : \"\"))",
        "-    .join(\"\\n\")",
        "-    .trim();",
        "-  const todoItems = items.filter((item) => item && typeof item === \"object\" && (item as { type?: unknown }).type === \"todo\");",
        "-  const todoDone = todoItems.filter(",
        "-    (item) => item && typeof item === \"object\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),",
        "-  ).length;",
        "-  return {",
        "-    wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,",
        "-    characterCount: text.length,",
        "-    blockCount: items.length,",
        "-    todoTotal: todoItems.length,",
        "-    todoDone,",
        "-  };",
        "-}",
        "-",
        "-function toIsoNow() {",
        "-  return new Date().toISOString();",
        "-}",
        "-",
        "-export function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {",
        "-  const iframeRef = useRef<HTMLIFrameElement | null>(null);",
        "-  const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);",
        "-  const registerBridge = useEditorBridgeStore((state) => state.registerBridge);",
        "-  const onSnapshotRef = useRef(props.onSnapshot);",
        "-  const onStatsChangeRef = useRef(props.onStatsChange);",
        "-  const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);",
        "-  const revisionRef = useRef<number | null>(props.initialRevision ?? null);",
        "-  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);",
        "-  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => {",
        "-    const initialDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;",
        "-    return initialDoc;",
        "-  });",
        "-",
        "-  const [reloadKey, setReloadKey] = useState(0);",
        "-  const [bridgeState, setBridgeState] = useState<LeptosTiptapHostBridgeState>({",
        "-    ready: false,",
        "-    runtimeName: RUNTIME_NAME,",
        "-    runtimeVersion: RUNTIME_VERSION,",
        "-    runtimeUrl,",
        "-    documentId: props.documentId,",
        "-    workspaceId: props.workspaceId,",
        "-    status: \"booting\",",
        "-    lastChangeAt: null,",
        "-    lastSaveRequestAt: null,",
        "-    lastError: null,",
        "-  });",
        "-",
        "-  const iframeSrc = useMemo(",
        "-    () => buildIframeSrc(runtimeUrl, props, runtimeDoc, reloadKey),",
        "-    [props, reloadKey, runtimeDoc, runtimeUrl],",
        "-  );",
        "-",
        "-  useEffect(() => {",
        "-    onSnapshotRef.current = props.onSnapshot;",
        "-    onStatsChangeRef.current = props.onStatsChange;",
        "-    onPersistedMetaChangeRef.current = props.onPersistedMetaChange;",
        "-    revisionRef.current = props.initialRevision ?? null;",
        "-    conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;",
        "-  }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);",
        "-",
        "-  useEffect(() => {",
        "-    setRuntimeDoc(tiptapDocFromBlocks(props.initialContent as Json) as Json);",
        "-    setReloadKey((value) => value + 1);",
        "-    setBridgeState((prev) => ({",
        "-      ...prev,",
        "-      ready: false,",
        "-      documentId: props.documentId,",
        "-      workspaceId: props.workspaceId,",
        "-      status: \"booting\",",
        "-      lastError: null,",
        "-    }));",
        "-  }, [props.documentId, props.initialContent, props.workspaceId]);",
        "-",
        "-  useEffect(() => {",
        "-    const bridge: EditorReferenceBridge = {",
        "-      insertInlineReference: () => ({ blockId: null }),",
        "-      insertEmbedReference: () => ({ blockId: null }),",
        "-      undo: () => {",
        "-        // 现阶段 iframe runtime 还没有父->子撤销命令，先保持最小 bridge 兼容。",
        "-      },",
        "-      redo: () => {",
        "-        // 现阶段 iframe runtime 还没有父->子重做命令，先保持最小 bridge 兼容。",
        "-      },",
        "-      getCursorBlockId: () => null,",
        "-      replaceWithSnapshot: (blocks: Json) => {",
        "-        setRuntimeDoc(tiptapDocFromBlocks(blocks) as Json);",
        "-        setReloadKey((value) => value + 1);",
        "-        setBridgeState((prev) => ({",
        "-          ...prev,",
        "-          ready: false,",
        "-          status: \"reloading\",",
        "-          lastError: null,",
        "-        }));",
        "-      },",
        "-    };",
        "-    registerBridge(bridge);",
        "-    return () => registerBridge(null);",
        "-  }, [registerBridge]);",
        "-",
        "-  useEffect(() => {",
        "-    let disposed = false;",
        "-    const expectedOrigin = (() => {",
        "-      try {",
        "-        return new URL(runtimeUrl).origin;",
        "-      } catch {",
        "-        return \"\";",
        "-      }",
        "-    })();",
        "-",
        "-    const publishSnapshot = (payload: HostDocumentPayload) => {",
        "-      const blocks = blocksFromTiptapDoc(payload.content) as Json;",
        "-      const stats = buildStats(blocks);",
        "-      onSnapshotRef.current?.({ blocks, stats });",
        "-      onStatsChangeRef.current?.(stats);",
        "-      return { blocks, stats };",
        "-    };",
        "-",
        "-    const saveSnapshot = async (payload: HostDocumentPayload) => {",
        "-      const { blocks } = publishSnapshot(payload);",
        "-      const response = await fetch(\"/api/documents/save\", {",
        "-        method: \"POST\",",
        "-        headers: { \"Content-Type\": \"application/json\" },",
        "-        body: JSON.stringify({",
        "-          documentId: props.documentId,",
        "-          workspaceId: props.workspaceId,",
        "-          revision: payload.meta?.revision ?? revisionRef.current,",
        "-          conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,",
        "-          snapshotCapturedAt: toIsoNow(),",
        "-          blockCount: Array.isArray(blocks) ? blocks.length : 0,",
        "-          content: blocks,",
        "-        }),",
        "-      });",
        "-      const nextMeta = await response.json().catch(() => null);",
        "-      if (!response.ok) {",
        "-        const errorMessage = typeof nextMeta?.error === \"string\" ? nextMeta.error : `保存失败（${response.status}）`;",
        "-        throw new Error(errorMessage);",
        "-      }",
        "-      const nextRevision = typeof nextMeta?.revision === \"number\" ? nextMeta.revision : payload.meta?.revision ?? null;",
        "-      const nextConflictDetectionKey =",
        "-        typeof nextMeta?.conflictDetectionKey === \"string\"",
        "-          ? nextMeta.conflictDetectionKey",
        "-          : payload.meta?.conflict_detection_key ?? null;",
        "-      revisionRef.current = nextRevision;",
        "-      conflictDetectionKeyRef.current = nextConflictDetectionKey;",
        "-      onPersistedMetaChangeRef.current?.({",
        "-        revision: nextRevision,",
        "-        conflictDetectionKey: nextConflictDetectionKey,",
        "-      });",
        "-    };",
        "-",
        "-    const handleMessage = (event: MessageEvent<HostEnvelope>) => {",
        "-      if (!expectedOrigin || event.origin !== expectedOrigin) {",
        "-        return;",
        "-      }",
        "-      if (event.source !== iframeRef.current?.contentWindow) {",
        "-        return;",
        "-      }",
        "-      if (!isBridgeEnvelope(event.data)) {",
        "-        return;",
        "-      }",
        "-      const eventName = typeof event.data.event === \"string\" ? event.data.event : \"\";",
        "-      const payload = (event.data.payload ?? {}) as HostDocumentPayload;",
        "-      if (eventName === READY_EVENT) {",
        "-        setBridgeState((prev) => ({ ...prev, ready: true, status: \"ready\", lastError: null }));",
        "-        return;",
        "-      }",
        "-      if (eventName === STATE_EVENT) {",
        "-        setBridgeState((prev) => ({ ...prev, ready: true, status: \"editing\", lastError: null }));",
        "-        return;",
        "-      }",
        "-      if (eventName === CHANGE_EVENT) {",
        "-        publishSnapshot(payload);",
        "-        setBridgeState((prev) => ({ ...prev, ready: true, status: \"dirty\", lastChangeAt: toIsoNow(), lastError: null }));",
        "-        return;",
        "-      }",
        "-      if (eventName === SAVE_REQUEST_EVENT) {",
        "-        setBridgeState((prev) => ({",
        "-          ...prev,",
        "-          ready: true,",
        "-          status: \"saving\",",
        "-          lastSaveRequestAt: toIsoNow(),",
        "-          lastError: null,",
        "-        }));",
        "-        void saveSnapshot(payload)",
        "-          .then(() => {",
        "-            if (disposed) return;",
        "-            setBridgeState((prev) => ({ ...prev, ready: true, status: \"saved\", lastError: null }));",
        "-          })",
        "-          .catch((error) => {",
        "-            if (disposed) return;",
        "-            setBridgeState((prev) => ({",
        "-              ...prev,",
        "-              ready: true,",
        "-              status: \"error\",",
        "-              lastError: error instanceof Error ? error.message : \"保存失败\",",
        "-            }));",
        "-          });",
        "-      }",
        "-    };",
        "-",
        "-    window.addEventListener(\"message\", handleMessage);",
        "-    return () => {",
        "-      disposed = true;",
        "-      window.removeEventListener(\"message\", handleMessage);",
        "-    };",
        "-  }, [props.documentId, props.workspaceId, runtimeUrl]);",
        "-",
        "-  return (",
        "-    <iframe",
        "-      key={`${props.documentId}:${reloadKey}`}",
        "-      ref={iframeRef}",
        "-      src={iframeSrc}",
        "-      title=\"Leptos Tiptap Editor\"",
        "-      className=\"h-full w-full border-0\"",
        "-      data-bridge-status={bridgeState.status}",
        "-      data-runtime-name={bridgeState.runtimeName}",
        "-      data-runtime-version={bridgeState.runtimeVersion}",
        "-    />",
        "-  );",
        "-}",
        "+\"use client\";",
        "+",
        "+import { useEffect, useMemo, useRef, useState } from \"react\";",
        "+import type {",
        "+  DocumentEditorHostProps,",
        "+  LeptosTiptapHostBridgeState,",
        "+} from \"@/components/editor/editor-host-types\";",
        "+import { blocksFromTiptapDoc, tiptapDocFromBlocks } from \"@/lib/documents/tiptap-content-converter\";",
        "+import { useEditorBridgeStore } from \"@/store/editor-bridge\";",
        "+import { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";",
        "+import type { EditorReferenceBridge } from \"@/store/editor-bridge\";",
        "+import type { DocumentStats } from \"@/types/page-options\";",
        "+import type { Json } from \"@/types/supabase\";",
        "+",
        "+const RUNTIME_PORT = 8123;",
        "+const RUNTIME_NAME = \"8123-leptos-tiptap-runtime\";",
        "+const RUNTIME_VERSION = \"1.1.0\";",
        "+const EVENT_PREFIX = \"mnote:leptos-tiptap-spike\";",
        "+const READY_EVENT = `${EVENT_PREFIX}:ready`;",
        "+const CHANGE_EVENT = `${EVENT_PREFIX}:change`;",
        "+const STATE_EVENT = `${EVENT_PREFIX}:state`;",
        "+const SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;",
        "+const HEIGHT_EVENT = `${EVENT_PREFIX}:height`;",
        "+const BOOTSTRAP_EVENT = `${EVENT_PREFIX}:bootstrap`;",
        "+const REPLACE_DOCUMENT_EVENT = `${EVENT_PREFIX}:replace-document`;",
        "+const PROTOCOL = \"mnote.leptos_tiptap.bridge.v1\";",
        "+const EMPTY_DOC = { type: \"doc\", content: [{ type: \"paragraph\", attrs: {}, content: [] }] } as const;",
        "+const FALLBACK_IFRAME_MIN_HEIGHT = 720;",
        "+",
        "+type HostEnvelope<T = unknown> = {",
        "+  protocol?: string;",
        "+  runtime?: string;",
        "+  version?: string;",
        "+  source?: string;",
        "+  event?: string;",
        "+  payload?: T;",
        "+};",
        "+",
        "+type HostDocumentPayload = {",
        "+  title?: string | null;",
        "+  content?: unknown;",
        "+  meta?: {",
        "+    dirty_count?: number;",
        "+    editor_focused?: boolean;",
        "+    slash_open?: boolean;",
        "+    toolbar_open?: boolean;",
        "+    revision?: number | null;",
        "+    conflict_detection_key?: string | null;",
        "+  };",
        "+};",
        "+",
        "+type HeightPayload = {",
        "+  height?: number;",
        "+};",
        "+",
        "+type BootstrapPayload = {",
        "+  documentId: string;",
        "+  workspaceId: string;",
        "+  title: string | null;",
        "+  content: unknown;",
        "+  revision: number | null;",
        "+  conflictDetectionKey: string | null;",
        "+  readOnly: boolean;",
        "+};",
        "+",
        "+function resolveRuntimeUrl() {",
        "+  if (typeof window === \"undefined\") {",
        "+    return `http://localhost:${RUNTIME_PORT}/`;",
        "+  }",
        "+  const cfg = getMnoteRuntimeConfig();",
        "+  if (cfg.mnoteWebBaseUrl) {",
        "+    try {",
        "+      const url = new URL(cfg.mnoteWebBaseUrl);",
        "+      url.port = String(RUNTIME_PORT);",
        "+      url.pathname = \"/\";",
        "+      url.search = \"\";",
        "+      url.hash = \"\";",
        "+      return url.toString();",
        "+    } catch {",
        "+      // ignore",
        "+    }",
        "+  }",
        "+  try {",
        "+    const url = new URL(window.location.href);",
        "+    url.port = String(RUNTIME_PORT);",
        "+    url.pathname = \"/\";",
        "+    url.search = \"\";",
        "+    url.hash = \"\";",
        "+    return url.toString();",
        "+  } catch {",
        "+    return `http://localhost:${RUNTIME_PORT}/`;",
        "+  }",
        "+}",
        "+",
        "+function normalizeRuntimeDoc(tiptapDoc: Json) {",
        "+  if (!tiptapDoc || typeof tiptapDoc !== \"object\") {",
        "+    return EMPTY_DOC;",
        "+  }",
        "+  const doc = tiptapDoc as { type?: unknown; content?: unknown };",
        "+  if (doc.type !== \"doc\") {",
        "+    return EMPTY_DOC;",
        "+  }",
        "+  if (!Array.isArray(doc.content) || doc.content.length === 0) {",
        "+    return EMPTY_DOC;",
        "+  }",
        "+  return tiptapDoc;",
        "+}",
        "+",
        "+function buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, reloadKey: number) {",
        "+  const url = new URL(runtimeUrl);",
        "+  url.searchParams.set(\"embedded\", \"1\");",
        "+  url.searchParams.set(\"host\", \"leptos_tiptap\");",
        "+  url.searchParams.set(\"documentId\", props.documentId);",
        "+  url.searchParams.set(\"workspaceId\", props.workspaceId);",
        "+  url.searchParams.set(\"reloadKey\", String(reloadKey));",
        "+  return url.toString();",
        "+}",
        "+",
        "+function isBridgeEnvelope(value: unknown): value is HostEnvelope {",
        "+  if (!value || typeof value !== \"object\") {",
        "+    return false;",
        "+  }",
        "+  const maybe = value as HostEnvelope;",
        "+  return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;",
        "+}",
        "+",
        "+function flattenText(value: unknown): string {",
        "+  if (typeof value === \"string\") return value;",
        "+  if (Array.isArray(value)) return value.map(flattenText).join(\"\");",
        "+  if (value && typeof value === \"object\") {",
        "+    const obj = value as { text?: unknown; content?: unknown };",
        "+    return `${flattenText(obj.text)}${flattenText(obj.content)}`;",
        "+  }",
        "+  return \"\";",
        "+}",
        "+",
        "+function buildStats(blocks: Json): DocumentStats {",
        "+  const items = Array.isArray(blocks) ? blocks : [];",
        "+  const text = items",
        "+    .map((item) => (item && typeof item === \"object\" ? flattenText((item as { content?: unknown }).content) : \"\"))",
        "+    .join(\"\\n\")",
        "+    .trim();",
        "+  const todoItems = items.filter((item) => item && typeof item === \"object\" && (item as { type?: unknown }).type === \"todo\");",
        "+  const todoDone = todoItems.filter(",
        "+    (item) => item && typeof item === \"object\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),",
        "+  ).length;",
        "+  return {",
        "+    wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,",
        "+    characterCount: text.length,",
        "+    blockCount: items.length,",
        "+    todoTotal: todoItems.length,",
        "+    todoDone,",
        "+  };",
        "+}",
        "+",
        "+function toIsoNow() {",
        "+  return new Date().toISOString();",
        "+}",
        "+",
        "+function coerceHeight(value: unknown): number {",
        "+  const height = Number(value);",
        "+  if (!Number.isFinite(height)) {",
        "+    return FALLBACK_IFRAME_MIN_HEIGHT;",
        "+  }",
        "+  return Math.max(FALLBACK_IFRAME_MIN_HEIGHT, Math.ceil(height));",
        "+}",
        "+",
        "+function postBridgeMessage(targetWindow: Window | null | undefined, event: string, payload: unknown) {",
        "+  if (!targetWindow) return;",
        "+  const envelope: HostEnvelope = {",
        "+    protocol: PROTOCOL,",
        "+    runtime: RUNTIME_NAME,",
        "+    version: RUNTIME_VERSION,",
        "+    source: EVENT_PREFIX,",
        "+    event,",
        "+    payload,",
        "+  };",
        "+  targetWindow.postMessage(envelope, \"*\");",
        "+}",
        "+",
        "+export function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {",
        "+  const iframeRef = useRef<HTMLIFrameElement | null>(null);",
        "+  const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);",
        "+  const registerBridge = useEditorBridgeStore((state) => state.registerBridge);",
        "+  const onSnapshotRef = useRef(props.onSnapshot);",
        "+  const onStatsChangeRef = useRef(props.onStatsChange);",
        "+  const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);",
        "+  const revisionRef = useRef<number | null>(props.initialRevision ?? null);",
        "+  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);",
        "+  const bootstrapPayloadRef = useRef<BootstrapPayload | null>(null);",
        "+  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => tiptapDocFromBlocks(props.initialContent as Json) as Json);",
        "+  const [reloadKey, setReloadKey] = useState(0);",
        "+  const [iframeHeight, setIframeHeight] = useState(FALLBACK_IFRAME_MIN_HEIGHT);",
        "+  const [bridgeState, setBridgeState] = useState<LeptosTiptapHostBridgeState>({",
        "+    ready: false,",
        "+    runtimeName: RUNTIME_NAME,",
        "+    runtimeVersion: RUNTIME_VERSION,",
        "+    runtimeUrl,",
        "+    documentId: props.documentId,",
        "+    workspaceId: props.workspaceId,",
        "+    status: \"booting\",",
        "+    lastChangeAt: null,",
        "+    lastSaveRequestAt: null,",
        "+    lastError: null,",
        "+  });",
        "+",
        "+  const iframeSrc = useMemo(",
        "+    () => buildIframeSrc(runtimeUrl, props, reloadKey),",
        "+    [props, reloadKey, runtimeUrl],",
        "+  );",
        "+",
        "+  useEffect(() => {",
        "+    onSnapshotRef.current = props.onSnapshot;",
        "+    onStatsChangeRef.current = props.onStatsChange;",
        "+    onPersistedMetaChangeRef.current = props.onPersistedMetaChange;",
        "+    revisionRef.current = props.initialRevision ?? null;",
        "+    conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;",
        "+  }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);",
        "+",
        "+  useEffect(() => {",
        "+    const nextRuntimeDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;",
        "+    setRuntimeDoc(nextRuntimeDoc);",
        "+    bootstrapPayloadRef.current = {",
        "+      documentId: props.documentId,",
        "+      workspaceId: props.workspaceId,",
        "+      title: props.title ?? null,",
        "+      content: normalizeRuntimeDoc(nextRuntimeDoc),",
        "+      revision: props.initialRevision ?? null,",
        "+      conflictDetectionKey: props.initialConflictDetectionKey ?? null,",
        "+      readOnly: Boolean(props.readOnly),",
        "+    };",
        "+    setIframeHeight(FALLBACK_IFRAME_MIN_HEIGHT);",
        "+    setReloadKey((value) => value + 1);",
        "+    setBridgeState((prev) => ({",
        "+      ...prev,",
        "+      ready: false,",
        "+      documentId: props.documentId,",
        "+      workspaceId: props.workspaceId,",
        "+      status: \"booting\",",
        "+      lastError: null,",
        "+    }));",
        "+  }, [props.documentId, props.initialContent, props.initialConflictDetectionKey, props.initialRevision, props.readOnly, props.title, props.workspaceId]);",
        "+",
        "+  useEffect(() => {",
        "+    const iframeWindow = iframeRef.current?.contentWindow;",
        "+    if (!iframeWindow) return;",
        "+    const payload = bootstrapPayloadRef.current;",
        "+    if (!payload) return;",
        "+    postBridgeMessage(iframeWindow, REPLACE_DOCUMENT_EVENT, payload);",
        "+    setBridgeState((prev) => ({ ...prev, status: prev.ready ? \"reloading\" : prev.status }));",
        "+  }, [runtimeDoc]);",
        "+",
        "+  useEffect(() => {",
        "+    const bridge: EditorReferenceBridge = {",
        "+      insertInlineReference: () => ({ blockId: null }),",
        "+      insertEmbedReference: () => ({ blockId: null }),",
        "+      undo: () => {",
        "+        // 现阶段 iframe runtime 还没有父->子撤销命令，先保持最小 bridge 兼容。",
        "+      },",
        "+      redo: () => {",
        "+        // 现阶段 iframe runtime 还没有父->子重做命令，先保持最小 bridge 兼容。",
        "+      },",
        "+      getCursorBlockId: () => null,",
        "+      replaceWithSnapshot: (blocks: Json) => {",
        "+        const nextRuntimeDoc = tiptapDocFromBlocks(blocks) as Json;",
        "+        setRuntimeDoc(nextRuntimeDoc);",
        "+        bootstrapPayloadRef.current = {",
        "+          documentId: props.documentId,",
        "+          workspaceId: props.workspaceId,",
        "+          title: props.title ?? null,",
        "+          content: normalizeRuntimeDoc(nextRuntimeDoc),",
        "+          revision: revisionRef.current,",
        "+          conflictDetectionKey: conflictDetectionKeyRef.current,",
        "+          readOnly: Boolean(props.readOnly),",
        "+        };",
        "+        postBridgeMessage(iframeRef.current?.contentWindow, REPLACE_DOCUMENT_EVENT, bootstrapPayloadRef.current);",
        "+        setBridgeState((prev) => ({",
        "+          ...prev,",
        "+          ready: false,",
        "+          status: \"reloading\",",
        "+          lastError: null,",
        "+        }));",
        "+      },",
        "+    };",
        "+    registerBridge(bridge);",
        "+    return () => registerBridge(null);",
        "+  }, [props.documentId, props.readOnly, props.title, props.workspaceId, registerBridge]);",
        "+",
        "+  useEffect(() => {",
        "+    let disposed = false;",
        "+    const expectedOrigin = (() => {",
        "+      try {",
        "+        return new URL(runtimeUrl).origin;",
        "+      } catch {",
        "+        return \"\";",
        "+      }",
        "+    })();",
        "+",
        "+    const publishSnapshot = (payload: HostDocumentPayload) => {",
        "+      const blocks = blocksFromTiptapDoc(payload.content) as Json;",
        "+      const stats = buildStats(blocks);",
        "+      setRuntimeDoc(normalizeRuntimeDoc(payload.content as Json) as Json);",
        "+      onSnapshotRef.current?.({ blocks, stats });",
        "+      onStatsChangeRef.current?.(stats);",
        "+      return { blocks, stats };",
        "+    };",
        "+",
        "+    const saveSnapshot = async (payload: HostDocumentPayload) => {",
        "+      const { blocks } = publishSnapshot(payload);",
        "+      const response = await fetch(\"/api/documents/save\", {",
        "+        method: \"POST\",",
        "+        headers: { \"Content-Type\": \"application/json\" },",
        "+        body: JSON.stringify({",
        "+          documentId: props.documentId,",
        "+          workspaceId: props.workspaceId,",
        "+          revision: payload.meta?.revision ?? revisionRef.current,",
        "+          conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,",
        "+          snapshotCapturedAt: toIsoNow(),",
        "+          blockCount: Array.isArray(blocks) ? blocks.length : 0,",
        "+          content: blocks,",
        "+        }),",
        "+      });",
        "+      const nextMeta = await response.json().catch(() => null);",
        "+      if (!response.ok) {",
        "+        const errorMessage = typeof nextMeta?.error === \"string\" ? nextMeta.error : `保存失败（${response.status}）`;",
        "+        throw new Error(errorMessage);",
        "+      }",
        "+      const nextRevision = typeof nextMeta?.revision === \"number\" ? nextMeta.revision : payload.meta?.revision ?? null;",
        "+      const nextConflictDetectionKey =",
        "+        typeof nextMeta?.conflictDetectionKey === \"string\"",
        "+          ? nextMeta.conflictDetectionKey",
        "+          : payload.meta?.conflict_detection_key ?? null;",
        "+      revisionRef.current = nextRevision;",
        "+      conflictDetectionKeyRef.current = nextConflictDetectionKey;",
        "+      onPersistedMetaChangeRef.current?.({",
        "+        revision: nextRevision,",
        "+        conflictDetectionKey: nextConflictDetectionKey,",
        "+      });",
        "+    };",
        "+",
        "+    const handleMessage = (event: MessageEvent<HostEnvelope>) => {",
        "+      if (!expectedOrigin || event.origin !== expectedOrigin) {",
        "+        return;",
        "+      }",
        "+      if (event.source !== iframeRef.current?.contentWindow) {",
        "+        return;",
        "+      }",
        "+      if (!isBridgeEnvelope(event.data)) {",
        "+        return;",
        "+      }",
        "+      const eventName = typeof event.data.event === \"string\" ? event.data.event : \"\";",
        "+      const payload = (event.data.payload ?? {}) as HostDocumentPayload & HeightPayload;",
        "+      if (eventName === READY_EVENT) {",
        "+        const bootstrapPayload = bootstrapPayloadRef.current;",
        "+        if (bootstrapPayload) {",
        "+          postBridgeMessage(iframeRef.current?.contentWindow, BOOTSTRAP_EVENT, bootstrapPayload);",
        "+        }",
        "+        setBridgeState((prev) => ({ ...prev, ready: true, status: \"ready\", lastError: null }));",
        "+        return;",
        "+      }",
        "+      if (eventName === STATE_EVENT) {",
        "+        setBridgeState((prev) => ({ ...prev, ready: true, status: \"editing\", lastError: null }));",
        "+        return;",
        "+      }",
        "+      if (eventName === HEIGHT_EVENT) {",
        "+        setIframeHeight(coerceHeight(payload.height));",
        "+        return;",
        "+      }",
        "+      if (eventName === CHANGE_EVENT) {",
        "+        publishSnapshot(payload);",
        "+        setBridgeState((prev) => ({ ...prev, ready: true, status: \"dirty\", lastChangeAt: toIsoNow(), lastError: null }));",
        "+        return;",
        "+      }",
        "+      if (eventName === SAVE_REQUEST_EVENT) {",
        "+        setBridgeState((prev) => ({",
        "+          ...prev,",
        "+          ready: true,",
        "+          status: \"saving\",",
        "+          lastSaveRequestAt: toIsoNow(),",
        "+          lastError: null,",
        "+        }));",
        "+        void saveSnapshot(payload)",
        "+          .then(() => {",
        "+            if (disposed) return;",
        "+            setBridgeState((prev) => ({ ...prev, ready: true, status: \"saved\", lastError: null }));",
        "+          })",
        "+          .catch((error) => {",
        "+            if (disposed) return;",
        "+            setBridgeState((prev) => ({",
        "+              ...prev,",
        "+              ready: true,",
        "+              status: \"error\",",
        "+              lastError: error instanceof Error ? error.message : \"保存失败\",",
        "+            }));",
        "+          });",
        "+      }",
        "+    };",
        "+",
        "+    window.addEventListener(\"message\", handleMessage);",
        "+    return () => {",
        "+      disposed = true;",
        "+      window.removeEventListener(\"message\", handleMessage);",
        "+    };",
        "+  }, [props.documentId, props.workspaceId, runtimeUrl]);",
        "+",
        "+  return (",
        "+    <iframe",
        "+      key={`${props.documentId}:${reloadKey}`}",
        "+      ref={iframeRef}",
        "+      src={iframeSrc}",
        "+      title=\"Leptos Tiptap Editor\"",
        "+      className=\"w-full border-0\"",
        "+      style={{ height: `${iframeHeight}px`, minHeight: `${FALLBACK_IFRAME_MIN_HEIGHT}px` }}",
        "+      data-bridge-status={bridgeState.status}",
        "+      data-runtime-name={bridgeState.runtimeName}",
        "+      data-runtime-version={bridgeState.runtimeVersion}",
        "+    />",
        "+  );",
        "+}"
      ]
    }
  ],
  "originalFile": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n  DocumentEditorHostProps,\n  LeptosTiptapHostBridgeState,\n} from \"@/components/editor/editor-host-types\";\nimport { blocksFromTiptapDoc, tiptapDocFromBlocks } from \"@/lib/documents/tiptap-content-converter\";\nimport { useEditorBridgeStore } from \"@/store/editor-bridge\";\nimport { getMnoteRuntimeConfig } from \"@/lib/runtime-config\";\nimport type { EditorReferenceBridge } from \"@/store/editor-bridge\";\nimport type { DocumentStats } from \"@/types/page-options\";\nimport type { Json } from \"@/types/supabase\";\n\nconst RUNTIME_PORT = 8123;\nconst RUNTIME_NAME = \"8123-leptos-tiptap-runtime\";\nconst RUNTIME_VERSION = \"1.0.0\";\nconst EVENT_PREFIX = \"mnote:leptos-tiptap-spike\";\nconst READY_EVENT = `${EVENT_PREFIX}:ready`;\nconst CHANGE_EVENT = `${EVENT_PREFIX}:change`;\nconst STATE_EVENT = `${EVENT_PREFIX}:state`;\nconst SAVE_REQUEST_EVENT = `${EVENT_PREFIX}:save-request`;\nconst PROTOCOL = \"mnote.leptos_tiptap.bridge.v1\";\nconst EMPTY_DOC = { type: \"doc\", content: [{ type: \"paragraph\" }] } as const;\n\ntype HostEnvelope<T = unknown> = {\n  protocol?: string;\n  runtime?: string;\n  version?: string;\n  source?: string;\n  event?: string;\n  payload?: T;\n};\n\ntype HostDocumentPayload = {\n  title?: string;\n  content?: unknown;\n  meta?: {\n    dirty_count?: number;\n    editor_focused?: boolean;\n    slash_open?: boolean;\n    toolbar_open?: boolean;\n    revision?: number | null;\n    conflict_detection_key?: string | null;\n  };\n};\n\nfunction resolveRuntimeUrl() {\n  if (typeof window === \"undefined\") {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n  const cfg = getMnoteRuntimeConfig();\n  if (cfg.mnoteWebBaseUrl) {\n    try {\n      const url = new URL(cfg.mnoteWebBaseUrl);\n      url.port = String(RUNTIME_PORT);\n      url.pathname = \"/\";\n      url.search = \"\";\n      url.hash = \"\";\n      return url.toString();\n    } catch {\n      // ignore\n    }\n  }\n  try {\n    const url = new URL(window.location.href);\n    url.port = String(RUNTIME_PORT);\n    url.pathname = \"/\";\n    url.search = \"\";\n    url.hash = \"\";\n    return url.toString();\n  } catch {\n    return `http://localhost:${RUNTIME_PORT}/`;\n  }\n}\n\nfunction normalizeRuntimeDoc(tiptapDoc: Json) {\n  if (!tiptapDoc || typeof tiptapDoc !== \"object\") {\n    return EMPTY_DOC;\n  }\n  const doc = tiptapDoc as { type?: unknown; content?: unknown };\n  if (doc.type !== \"doc\") {\n    return EMPTY_DOC;\n  }\n  if (!Array.isArray(doc.content) || doc.content.length === 0) {\n    return EMPTY_DOC;\n  }\n  return tiptapDoc;\n}\n\nfunction buildIframeSrc(runtimeUrl: string, props: DocumentEditorHostProps, tiptapDoc: Json, reloadKey: number) {\n  const url = new URL(runtimeUrl);\n  url.searchParams.set(\"embedded\", \"1\");\n  url.searchParams.set(\"host\", \"leptos_tiptap\");\n  url.searchParams.set(\"documentId\", props.documentId);\n  url.searchParams.set(\"workspaceId\", props.workspaceId);\n  url.searchParams.set(\"reloadKey\", String(reloadKey));\n  url.searchParams.set(\"content\", JSON.stringify(normalizeRuntimeDoc(tiptapDoc)));\n  if (props.initialRevision != null) {\n    url.searchParams.set(\"revision\", String(props.initialRevision));\n  }\n  if (props.initialConflictDetectionKey) {\n    url.searchParams.set(\"conflictDetectionKey\", props.initialConflictDetectionKey);\n  }\n  return url.toString();\n}\n\nfunction isBridgeEnvelope(value: unknown): value is HostEnvelope {\n  if (!value || typeof value !== \"object\") {\n    return false;\n  }\n  const maybe = value as HostEnvelope;\n  return maybe.protocol === PROTOCOL && maybe.runtime === RUNTIME_NAME && maybe.source === EVENT_PREFIX;\n}\n\nfunction flattenText(value: unknown): string {\n  if (typeof value === \"string\") return value;\n  if (Array.isArray(value)) return value.map(flattenText).join(\"\");\n  if (value && typeof value === \"object\") {\n    const obj = value as { text?: unknown; content?: unknown };\n    return `${flattenText(obj.text)}${flattenText(obj.content)}`;\n  }\n  return \"\";\n}\n\nfunction buildStats(blocks: Json): DocumentStats {\n  const items = Array.isArray(blocks) ? blocks : [];\n  const text = items\n    .map((item) => (item && typeof item === \"object\" ? flattenText((item as { content?: unknown }).content) : \"\"))\n    .join(\"\\n\")\n    .trim();\n  const todoItems = items.filter((item) => item && typeof item === \"object\" && (item as { type?: unknown }).type === \"todo\");\n  const todoDone = todoItems.filter(\n    (item) => item && typeof item === \"object\" && Boolean((item as { props?: { checked?: unknown } }).props?.checked),\n  ).length;\n  return {\n    wordCount: text ? text.split(/\\s+/).filter(Boolean).length : 0,\n    characterCount: text.length,\n    blockCount: items.length,\n    todoTotal: todoItems.length,\n    todoDone,\n  };\n}\n\nfunction toIsoNow() {\n  return new Date().toISOString();\n}\n\nexport function LeptosTiptapEditorHost(props: DocumentEditorHostProps) {\n  const iframeRef = useRef<HTMLIFrameElement | null>(null);\n  const runtimeUrl = useMemo(() => resolveRuntimeUrl(), []);\n  const registerBridge = useEditorBridgeStore((state) => state.registerBridge);\n  const onSnapshotRef = useRef(props.onSnapshot);\n  const onStatsChangeRef = useRef(props.onStatsChange);\n  const onPersistedMetaChangeRef = useRef(props.onPersistedMetaChange);\n  const revisionRef = useRef<number | null>(props.initialRevision ?? null);\n  const conflictDetectionKeyRef = useRef<string | null>(props.initialConflictDetectionKey ?? null);\n  const [runtimeDoc, setRuntimeDoc] = useState<Json>(() => {\n    const initialDoc = tiptapDocFromBlocks(props.initialContent as Json) as Json;\n    return initialDoc;\n  });\n\n  const [reloadKey, setReloadKey] = useState(0);\n  const [bridgeState, setBridgeState] = useState<LeptosTiptapHostBridgeState>({\n    ready: false,\n    runtimeName: RUNTIME_NAME,\n    runtimeVersion: RUNTIME_VERSION,\n    runtimeUrl,\n    documentId: props.documentId,\n    workspaceId: props.workspaceId,\n    status: \"booting\",\n    lastChangeAt: null,\n    lastSaveRequestAt: null,\n    lastError: null,\n  });\n\n  const iframeSrc = useMemo(\n    () => buildIframeSrc(runtimeUrl, props, runtimeDoc, reloadKey),\n    [props, reloadKey, runtimeDoc, runtimeUrl],\n  );\n\n  useEffect(() => {\n    onSnapshotRef.current = props.onSnapshot;\n    onStatsChangeRef.current = props.onStatsChange;\n    onPersistedMetaChangeRef.current = props.onPersistedMetaChange;\n    revisionRef.current = props.initialRevision ?? null;\n    conflictDetectionKeyRef.current = props.initialConflictDetectionKey ?? null;\n  }, [props.initialConflictDetectionKey, props.initialRevision, props.onPersistedMetaChange, props.onSnapshot, props.onStatsChange]);\n\n  useEffect(() => {\n    setRuntimeDoc(tiptapDocFromBlocks(props.initialContent as Json) as Json);\n    setReloadKey((value) => value + 1);\n    setBridgeState((prev) => ({\n      ...prev,\n      ready: false,\n      documentId: props.documentId,\n      workspaceId: props.workspaceId,\n      status: \"booting\",\n      lastError: null,\n    }));\n  }, [props.documentId, props.initialContent, props.workspaceId]);\n\n  useEffect(() => {\n    const bridge: EditorReferenceBridge = {\n      insertInlineReference: () => ({ blockId: null }),\n      insertEmbedReference: () => ({ blockId: null }),\n      undo: () => {\n        // 现阶段 iframe runtime 还没有父->子撤销命令，先保持最小 bridge 兼容。\n      },\n      redo: () => {\n        // 现阶段 iframe runtime 还没有父->子重做命令，先保持最小 bridge 兼容。\n      },\n      getCursorBlockId: () => null,\n      replaceWithSnapshot: (blocks: Json) => {\n        setRuntimeDoc(tiptapDocFromBlocks(blocks) as Json);\n        setReloadKey((value) => value + 1);\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: false,\n          status: \"reloading\",\n          lastError: null,\n        }));\n      },\n    };\n    registerBridge(bridge);\n    return () => registerBridge(null);\n  }, [registerBridge]);\n\n  useEffect(() => {\n    let disposed = false;\n    const expectedOrigin = (() => {\n      try {\n        return new URL(runtimeUrl).origin;\n      } catch {\n        return \"\";\n      }\n    })();\n\n    const publishSnapshot = (payload: HostDocumentPayload) => {\n      const blocks = blocksFromTiptapDoc(payload.content) as Json;\n      const stats = buildStats(blocks);\n      onSnapshotRef.current?.({ blocks, stats });\n      onStatsChangeRef.current?.(stats);\n      return { blocks, stats };\n    };\n\n    const saveSnapshot = async (payload: HostDocumentPayload) => {\n      const { blocks } = publishSnapshot(payload);\n      const response = await fetch(\"/api/documents/save\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          documentId: props.documentId,\n          workspaceId: props.workspaceId,\n          revision: payload.meta?.revision ?? revisionRef.current,\n          conflictDetectionKey: payload.meta?.conflict_detection_key ?? conflictDetectionKeyRef.current,\n          snapshotCapturedAt: toIsoNow(),\n          blockCount: Array.isArray(blocks) ? blocks.length : 0,\n          content: blocks,\n        }),\n      });\n      const nextMeta = await response.json().catch(() => null);\n      if (!response.ok) {\n        const errorMessage = typeof nextMeta?.error === \"string\" ? nextMeta.error : `保存失败（${response.status}）`;\n        throw new Error(errorMessage);\n      }\n      const nextRevision = typeof nextMeta?.revision === \"number\" ? nextMeta.revision : payload.meta?.revision ?? null;\n      const nextConflictDetectionKey =\n        typeof nextMeta?.conflictDetectionKey === \"string\"\n          ? nextMeta.conflictDetectionKey\n          : payload.meta?.conflict_detection_key ?? null;\n      revisionRef.current = nextRevision;\n      conflictDetectionKeyRef.current = nextConflictDetectionKey;\n      onPersistedMetaChangeRef.current?.({\n        revision: nextRevision,\n        conflictDetectionKey: nextConflictDetectionKey,\n      });\n    };\n\n    const handleMessage = (event: MessageEvent<HostEnvelope>) => {\n      if (!expectedOrigin || event.origin !== expectedOrigin) {\n        return;\n      }\n      if (event.source !== iframeRef.current?.contentWindow) {\n        return;\n      }\n      if (!isBridgeEnvelope(event.data)) {\n        return;\n      }\n      const eventName = typeof event.data.event === \"string\" ? event.data.event : \"\";\n      const payload = (event.data.payload ?? {}) as HostDocumentPayload;\n      if (eventName === READY_EVENT) {\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"ready\", lastError: null }));\n        return;\n      }\n      if (eventName === STATE_EVENT) {\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"editing\", lastError: null }));\n        return;\n      }\n      if (eventName === CHANGE_EVENT) {\n        publishSnapshot(payload);\n        setBridgeState((prev) => ({ ...prev, ready: true, status: \"dirty\", lastChangeAt: toIsoNow(), lastError: null }));\n        return;\n      }\n      if (eventName === SAVE_REQUEST_EVENT) {\n        setBridgeState((prev) => ({\n          ...prev,\n          ready: true,\n          status: \"saving\",\n          lastSaveRequestAt: toIsoNow(),\n          lastError: null,\n        }));\n        void saveSnapshot(payload)\n          .then(() => {\n            if (disposed) return;\n            setBridgeState((prev) => ({ ...prev, ready: true, status: \"saved\", lastError: null }));\n          })\n          .catch((error) => {\n            if (disposed) return;\n            setBridgeState((prev) => ({\n              ...prev,\n              ready: true,\n              status: \"error\",\n              lastError: error instanceof Error ? error.message : \"保存失败\",\n            }));\n          });\n      }\n    };\n\n    window.addEventListener(\"message\", handleMessage);\n    return () => {\n      disposed = true;\n      window.removeEventListener(\"message\", handleMessage);\n    };\n  }, [props.documentId, props.workspaceId, runtimeUrl]);\n\n  return (\n    <iframe\n      key={`${props.documentId}:${reloadKey}`}\n      ref={iframeRef}\n      src={iframeSrc}\n      title=\"Leptos Tiptap Editor\"\n      className=\"h-full w-full border-0\"\n      data-bridge-status={bridgeState.status}\n      data-runtime-name={bridgeState.runtimeName}\n      data-runtime-version={bridgeState.runtimeVersion}\n    />\n  );\n}\n",
  "gitDiff": null
}


bash

Input: {"allowedMounts":["/mnt/Data1T/mnote"],"command":"python3 - <<'PY'\nfrom pathlib import Path\npath = Path('/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs')\ntext = path.read_text()\nfor needle in ['fn main()', 'fn initial_content()', 'let is_embedded', 'let json_output', 'selection_text =', 'view! {', 'window()', 'on_cleanup', 'set_json_output']:\n    idx = text.find(needle)\n    print('\\n===', needle, idx)\n    if idx != -1:\n        start=max(0, idx-250)\n        end=min(len(text), idx+700)\n        print(text[start:end])\nPY","dangerouslyDisableSandbox":false,"description":"Inspect spike main.rs around initialization and UI state for bridge integration","filesystemMode":"workspace-only","isolateNetwork":true,"namespaceRestrictions":true,"run_in_background":false,"timeout":120000}


Output:
{
  "stdout": "\n=== fn main() 151038\n}
\n
\n
\n            \n        \n    }\n}\n\nfn initial_content() -> TiptapContent {\n    TiptapContent::json(serde_json::json!({\n        \"type\": \"doc\",\n        \"content\": []\n    }))\n}\n\nfn main() {\n    console_error_panic_hook::set_once();\n    mount_to_body(|| {\n        view! { <App/> }\n    });\n}\n\n\n=== fn initial_content() 150898\n class=\"debug-card\">\n
\"JSON\"
\n
{move || json_output.get()}
\n
\n
\n            \n        \n    }\n}\n\nfn initial_content() -> TiptapContent {\n    TiptapContent::json(serde_json::json!({\n        \"type\": \"doc\",\n        \"content\": []\n    }))\n}\n\nfn main() {\n    console_error_panic_hook::set_once();\n    mount_to_body(|| {\n        view! { <App/> }\n    });\n}\n\n\n=== let is_embedded 75061\n  }\n\n    let has_task_schema = move || {\n        let snapshot = json_output.get();\n        snapshot.contains(\"\\\"taskList\\\"\") && snapshot.contains(\"\\\"taskItem\\\"\")\n    };\n\n    let selection_text = move || selection_summary(&selection_state.get());\n    let is_embedded = window()\n        .and_then(|win| win.location().search().ok())\n        .map(|search| search.contains(\"embedded=1\"))\n        .unwrap_or(false);\n\n    let on_link_click = {\n        let editor = editor;\n        let selection_state = selection_state;\n        let set_command_feedback = set_command_feedback;\n        move |_| {\n            if selection_state.get().link {\n                apply_feedback_result(\n                    set_command_feedback,\n                    editor.unset_link().map(|_| \"已移除链接\"),\n                );\n                return;\n            }\n\n            let href = window()\n                .and_then(|win| win.prompt_with_message(\"输入链接地址\").ok().flatten())\n     \n\n=== let json_output -1\n\n=== selection_text = 74989\n      }\n        });\n        on_cleanup(move || drop(keydown_handle));\n    }\n\n    let has_task_schema = move || {\n        let snapshot = json_output.get();\n        snapshot.contains(\"\\\"taskList\\\"\") && snapshot.contains(\"\\\"taskItem\\\"\")\n    };\n\n    let selection_text = move || selection_summary(&selection_state.get());\n    let is_embedded = window()\n        .and_then(|win| win.location().search().ok())\n        .map(|search| search.contains(\"embedded=1\"))\n        .unwrap_or(false);\n\n    let on_link_click = {\n        let editor = editor;\n        let selection_state = selection_state;\n        let set_command_feedback = set_command_feedback;\n        move |_| {\n            if selection_state.get().link {\n                apply_feedback_result(\n                    set_command_feedback,\n                    editor.unset_link().map(|_| \"已移除链接\"),\n                );\n                return;\n            }\n\n            let href = window()\n               \n\n=== view! { 76427\n\".into()),\n                            class: None,\n                        })\n                        .map(|_| \"已插入链接\"),\n                ),\n                None => set_command_feedback.set(\"链接操作已取消\".to_string()),\n            }\n        }\n    };\n\n    view! {\n        \n        \n
\n
\n                    \"P0 / Tiptap-like 主编辑器体验\"\n
\"把 Spike 收敛成能检验的最小产品页壳\"
\n
\n                        \"这里继续只做 \"\n                        \"8123\"\n                        \" 的 Leptos + Tiptap P0 体验验证，不挪动正式 \"\n                        \"/documents/[id]\"\n                        \" 主链。目标是先把 slash、toolbar、todo schema、块手柄和 reload 做成可直接看得见、点得动、能刷新保留结构\n\n=== window() 23853\nx: usize,\n    top: f64,\n    placement: DropPlacement,\n}\n\n#[derive(Clone, Debug, PartialEq)]\nstruct PendingDragState {\n    index: usize,\n    anchor: HoveredBlockState,\n    start_x: i32,\n    start_y: i32,\n}\n\nfn local_storage() -> Option<Storage> {\n    window().and_then(|win| win.local_storage().ok().flatten())\n}\n\nfn default_title() -> String {\n    \"Leptos Tiptap 主编辑器 P0\".to_string()\n}\n\nfn persist_document_state(\n    title: &str,\n    content: &Value,\n    html: Option<String>,\n) -> Result<(), String> {\n    let payload = PersistedSpikeDocument {\n        title: title.to_string(),\n        content: content.clone(),\n        html,\n    };\n    let raw = serde_json::to_string(&payload).map_err(|err| format!(\"序列化草稿失败：{err}\"))?;\n    local_storage()\n        .ok_or_else(|| \"浏览器 localStorage 不可用\".to_string())?\n        .set_item(SPIKE_STORAGE_KEY, &raw)\n        .map_err(|err| format!(\"写入 localStorage 失败：{err:?}\"))\n}\n\nfn load_persisted_document() -> Option\n\n=== on_cleanup 69326\n      set_floating_toolbar_anchor,\n                set_slash_open,\n                set_turn_into_open,\n                set_hovered_block,\n                set_block_menu_anchor,\n                set_block_menu_open,\n            );\n        });\n\n        on_cleanup(move || {\n            drop(mouseup_handle);\n            drop(keyup_handle);\n        });\n    }\n\n    {\n        let set_editor_focused = set_editor_focused;\n        let set_slash_open = set_slash_open;\n        let set_turn_into_open = set_turn_into_open;\n        let set_hovered_block = set_hovered_block;\n        let set_block_menu_anchor = set_block_menu_anchor;\n        let set_block_menu_open = set_block_menu_open;\n        let set_pending_drag = set_pending_drag;\n        let set_dragging_block_index = set_dragging_block_index;\n        let set_dragging_block_anchor = set_dragging_block_anchor;\n        let set_drop_indicator = set_drop_indicator;\n        let set_command_feedback = set\n\n=== set_json_output 25388\nvalue)\n        .unwrap_or_else(|err| format!(\"格式化 JSON 失败：{err}\"));\n    (html, json_value, json_text)\n}\n\nfn sync_editor_outputs(\n    editor: TiptapEditorHandle,\n    set_html_output: WriteSignal<String>,\n    set_document_json: WriteSignal<Value>,\n    set_json_output: WriteSignal<String>,\n) -> Value {\n    let (html, json_value, json_text) = read_editor_snapshot(editor);\n    set_html_output.set(html);\n    set_document_json.set(json_value.clone());\n    set_json_output.set(json_text);\n    json_value\n}\n\nfn editor_root_element() -> Option<Element> {\n    window()\n        .and_then(|win| win.document())\n        .and_then(|document| document.query_selector(EDITOR_ROOT_SELECTOR).ok().flatten())\n}\n\nfn editor_stage_element() -> Option<Element> {\n    window()\n        .and_then(|win| win.document())\n        .and_then(|document| {\n            document\n                .query_selector(EDITOR_STAGE_SELECTOR)\n                .ok()\n                .flatten(\n",
  "stderr": "",
  "rawOutputPath": null,
  "interrupted": false,
  "isImage": null,
  "backgroundTaskId": null,
  "backgroundedByUser": null,
  "assistantAutoBackgrounded": null,
  "dangerouslyDisableSandbox": false,
  "returnCodeInterpretation": null,
  "noOutputExpected": false,
  "structuredContent": null,
  "persistedOutputPath": null,
  "persistedOutputSize": null,
  "sandboxStatus": {
    "enabled": true,
    "requested": {
      "enabled": true,
      "namespace_restrictions": true,
      "network_isolation": true,
      "filesystem_mode": "workspace-only",
      "allowed_mounts": [
        "/mnt/Data1T/mnote"
      ]
    },
    "supported": true,
    "active": true,
    "namespace_supported": true,
    "namespace_active": true,
    "network_supported": true,
    "network_active": true,
    "filesystem_mode": "workspace-only",
    "filesystem_active": true,
    "allowed_mounts": [
      "/mnt/Data1T/mnote"
    ],
    "in_container": false,
    "container_markers": [],
    "fallback_reason": null
  }
}


Input: {"allowedMounts":["/mnt/Data1T/mnote"],"command":"python3 - <<'PY'\nfrom pathlib import Path\ntext = Path('/mnt/Data1T/mnote/rust/spikes/leptos-tiptap-spike/src/main.rs').read_text()\nneedles = ['SPIKE_STORAGE_KEY', 'load_persisted_document()', 'sync_editor_outputs(', 'let (html_output', 'let (document_json', 'let (json_output', 'create_effect(move ||', 'on_cleanup(move || drop(keydown_handle));']\nfor needle in needles:\n    idx = text.find(needle)\n    print('\\n===', needle, idx)\n    if idx != -1:\n        start=max(0, idx-350)\n        end=min(len(text), idx+1200)\n        print(text[start:end])\nPY","dangerouslyDisableSandbox":false,"description":"Inspect spike state persistence and signals for runtime bridge integration","filesystemMode":"workspace-only","isolateNetwork":true,"namespaceRestrictions":true,"run_in_background":false,"timeout":120000}