diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index 1ce631cc..81a5c6e2 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -380,6 +380,16 @@ fn build_tree_shell_html( background: rgba(15, 108, 132, 0.1); border-color: rgba(15, 108, 132, 0.24); } + .tree-row[data-drop-feedback="true"] { + background: rgba(148, 163, 184, 0.18); + border-color: rgba(15, 108, 132, 0.28); + } + .tree-row[data-draggable="true"] { + cursor: grab; + } + .tree-row[data-draggable="true"]:active { + cursor: grabbing; + } .tree-toggle, .tree-action { border: 0; @@ -588,6 +598,13 @@ fn build_tree_shell_html( box-shadow: none; background: rgba(37, 99, 235, 0.12); } + .tree-row[data-drop-target="true"] { + background: rgba(15, 108, 132, 0.14); + outline: 1px solid rgba(15, 108, 132, 0.35); + } + .tree-root[data-drop-target="true"] { + background: rgba(15, 108, 132, 0.05); + } .tree-toggle, .tree-action { border-radius: 4px; @@ -738,6 +755,11 @@ fn build_tree_shell_html( }; const state = parseState(); + const hostOverride = + window.__MNOTE_TREE_SHELL_OVERRIDE__ && + typeof window.__MNOTE_TREE_SHELL_OVERRIDE__ === "object" + ? window.__MNOTE_TREE_SHELL_OVERRIDE__ + : {}; const channel = typeof state.channel === "string" && state.channel.trim() ? state.channel.trim() @@ -807,17 +829,68 @@ fn build_tree_shell_html( return Number.isFinite(value) ? Number(value) : fallback; }; - const normalizedItems = Array.isArray(state.items) - ? state.items - .map((item) => ({ - nodeId: normalizeText(item?.nodeId), - parentNodeId: normalizeParent(item?.parentNodeId), - title: normalizeText(item?.title, "无标题"), - depth: normalizeNumber(item?.depth, 0), - childCount: normalizeNumber(item?.childCount, 0), - position: normalizeNumber(item?.position), - expandedByDefault: item?.expandedByDefault !== false, - })) + const normalizeRowKind = (value) => { + const normalized = normalizeText(value).toLowerCase(); + if (normalized === "index") return "index"; + if (normalized === "asset") return "asset"; + if (normalized === "asset_folder") return "asset_folder"; + return "document"; + }; + + const normalizeCapabilities = (value) => + Array.isArray(value) + ? value + .map((item) => normalizeText(item)) + .filter(Boolean) + : []; + + const normalizeResourceMeta = (value) => { + if (!value || typeof value !== "object") { + return { + resourceKind: "", + documentId: "", + assetId: "", + assetKind: "", + }; + } + return { + resourceKind: normalizeText(value?.resourceKind), + documentId: normalizeText(value?.documentId), + assetId: normalizeText(value?.assetId), + assetKind: normalizeText(value?.assetKind), + }; + }; + + const rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items; + const normalizedItems = Array.isArray(rawItems) + ? rawItems + .map((item) => { + const nodeId = normalizeText(item?.nodeId); + const resourceMeta = normalizeResourceMeta(item?.resourceMeta); + const rowKind = normalizeRowKind(item?.rowKind); + const fallbackRowId = + rowKind === "index" + ? `index:${resourceMeta.documentId || nodeId.replace(/^index:/, "")}` + : rowKind === "asset" + ? `asset:${resourceMeta.assetId || nodeId.replace(/^asset:/, "")}` + : rowKind === "asset_folder" + ? `asset-folder:${resourceMeta.assetId || nodeId.replace(/^asset-folder:/, "")}` + : `doc:${resourceMeta.documentId || nodeId}`; + return { + rowId: normalizeText(item?.rowId, fallbackRowId), + rowKind, + nodeId, + parentNodeId: normalizeParent(item?.parentNodeId), + title: normalizeText(item?.title, rowKind === "index" ? "index.md" : "无标题"), + depth: normalizeNumber(item?.depth, 0), + childCount: normalizeNumber(item?.childCount, 0), + position: normalizeNumber(item?.position), + expandedByDefault: item?.expandedByDefault !== false, + iconHint: normalizeText(item?.iconHint), + capabilities: normalizeCapabilities(item?.capabilities), + resourceMeta, + }; + }) .filter((item) => item.nodeId && !excludedIds.has(item.nodeId)) : []; @@ -870,6 +943,12 @@ fn build_tree_shell_html( ? activeDocumentId : roots[0]?.nodeId || ""; let selectedFileTreeRowIds = new Set(activeDocumentId ? [`doc:${activeDocumentId}`, `index:${activeDocumentId}`] : []); + let fileTreeAnchorRowId = activeDocumentId ? `doc:${activeDocumentId}` : null; + let fileTreeFocusedRowId = activeDocumentId ? `doc:${activeDocumentId}` : null; + let visibleFileTreeRowIds = []; + let draggingFileTreeRowIds = []; + let activeFileTreeDropRowId = null; + let activeFileTreeRootDrop = false; let activeCursor = itemById.get(activeDocumentId) || null; while (activeCursor && activeCursor.parentNodeId && itemById.has(activeCursor.parentNodeId)) { @@ -938,6 +1017,316 @@ fn build_tree_shell_html( window.parent.postMessage(payload, targetOrigin); }; + const FILETREE_DRAG_MIME = "application/x-mnote-filetree-row-ids"; + + const getFileTreeRowDocumentId = (item) => { + if (!item) return ""; + if (item.resourceMeta?.documentId) return item.resourceMeta.documentId; + if (item.rowKind === "document") return item.nodeId; + if (item.rowKind === "index") return item.nodeId.replace(/^index:/, ""); + return ""; + }; + + const getFileTreeRowAssetId = (item) => { + if (!item) return ""; + if (item.resourceMeta?.assetId) return item.resourceMeta.assetId; + if (item.rowKind === "asset") return item.nodeId.replace(/^asset:/, ""); + if (item.rowKind === "asset_folder") return item.nodeId.replace(/^asset-folder:/, ""); + return ""; + }; + + const getFileTreeRowIconKind = (item) => { + if (!item) return "file"; + const iconHint = normalizeText(item.iconHint).toLowerCase(); + if (iconHint === "mindmap") return "mindmap"; + if (iconHint === "table") return "table"; + if (iconHint === "index") return "index"; + if (iconHint === "page") return "page"; + if (item.rowKind === "document") return "page"; + if (item.rowKind === "index") return "index"; + if (item.rowKind === "asset_folder") return "mindmap"; + if (item.resourceMeta?.resourceKind === "table") return "table"; + if (item.resourceMeta?.resourceKind === "mindmap") return "mindmap"; + return "file"; + }; + + const getFileTreeRowMetaLabel = (item) => { + if (!item) return ""; + if (item.rowKind === "document") { + return `${getFileTreeRowDocumentId(item)} · 页面`; + } + if (item.rowKind === "index") { + return "页面正文"; + } + if (item.rowKind === "asset_folder") { + return `${item.resourceMeta?.resourceKind || "mindmap"} · 资源目录`; + } + return `${item.resourceMeta?.resourceKind || item.resourceMeta?.assetKind || "asset"} · ${getFileTreeRowAssetId(item)}`; + }; + + const canExpandFileTreeRow = (item) => { + if (!item) return false; + return item.childCount > 0 || item.capabilities.includes("expand"); + }; + + const getFileTreeDropTargetFromElement = (element) => { + const row = element instanceof Element + ? element.closest('.tree-row[data-shell-mode="filetree"]') + : null; + if (!(row instanceof HTMLElement)) { + return { + rowId: null, + rowKind: "root", + documentId: null, + assetId: null, + }; + } + return { + rowId: normalizeText(row.dataset.rowId) || null, + rowKind: normalizeText(row.dataset.rowKind, "document"), + documentId: normalizeText(row.dataset.documentId) || null, + assetId: normalizeText(row.dataset.assetId) || null, + }; + }; + + const clearFileTreeDropFeedback = () => { + if (activeFileTreeDropRowId) { + const previousRow = appElement.querySelector( + `.tree-row[data-shell-mode="filetree"][data-row-id="${activeFileTreeDropRowId}"]`, + ); + if (previousRow instanceof HTMLElement) { + previousRow.dataset.dropTarget = "false"; + } + } + activeFileTreeDropRowId = null; + activeFileTreeRootDrop = false; + appElement.querySelectorAll('.tree-root[data-drop-target="true"]').forEach((element) => { + if (element instanceof HTMLElement) { + element.dataset.dropTarget = "false"; + } + }); + }; + + const setFileTreeDropFeedback = (target) => { + const nextRowId = target?.rowId || null; + if (activeFileTreeDropRowId && activeFileTreeDropRowId !== nextRowId) { + const previousRow = appElement.querySelector( + `.tree-row[data-shell-mode="filetree"][data-row-id="${activeFileTreeDropRowId}"]`, + ); + if (previousRow instanceof HTMLElement) { + previousRow.dataset.dropTarget = "false"; + } + } + if (nextRowId) { + const nextRow = appElement.querySelector( + `.tree-row[data-shell-mode="filetree"][data-row-id="${nextRowId}"]`, + ); + if (nextRow instanceof HTMLElement) { + nextRow.dataset.dropTarget = "true"; + } + activeFileTreeDropRowId = nextRowId; + activeFileTreeRootDrop = false; + appElement.querySelectorAll('.tree-root[data-drop-target="true"]').forEach((element) => { + if (element instanceof HTMLElement) { + element.dataset.dropTarget = "false"; + } + }); + return; + } + activeFileTreeDropRowId = null; + const root = appElement.querySelector(".tree-root"); + if (root instanceof HTMLElement) { + root.dataset.dropTarget = "true"; + } + activeFileTreeRootDrop = true; + }; + + const updateFileTreeDropFeedback = (rowId) => { + if (rowId) { + setFileTreeDropFeedback({ rowId }); + return; + } + clearFileTreeDropFeedback(); + }; + + const inferDefaultFileTreeDropDocumentId = () => { + const candidateRowIds = [ + fileTreeFocusedRowId, + fileTreeAnchorRowId, + ...Array.from(selectedFileTreeRowIds), + ].filter(Boolean); + + for (const rowId of candidateRowIds) { + if (!rowId) continue; + const item = fileTreeRowById.get(rowId); + const documentId = getFileTreeRowDocumentId(item); + if (documentId) { + return documentId; + } + } + + const firstDocRowId = visibleFileTreeRowIds.find((rowId) => rowId.startsWith("doc:")); + const firstDocItem = firstDocRowId ? fileTreeRowById.get(firstDocRowId) : null; + return getFileTreeRowDocumentId(firstDocItem) || null; + }; + + const resolveFileTreeDraggedRowIds = (rowId) => { + if (!rowId) return []; + if (selectedFileTreeRowIds.has(rowId)) { + return Array.from(selectedFileTreeRowIds); + } + return [rowId]; + }; + + const isExternalFileDrag = (event) => { + const types = event.dataTransfer?.types; + return Array.isArray(types) + ? types.includes("Files") + : types instanceof DOMStringList + ? types.contains("Files") + : false; + }; + + const isInternalFileTreeDrag = (event) => { + const types = event.dataTransfer?.types; + return Array.isArray(types) + ? types.includes(FILETREE_DRAG_MIME) + : types instanceof DOMStringList + ? types.contains(FILETREE_DRAG_MIME) + : false; + }; + + const emitFileTreeSelectionChange = () => { + if (mode !== "filetree") return; + postToHost("tree.filetree.selection.changed", { + selectedRowIds: Array.from(selectedFileTreeRowIds), + anchorRowId: fileTreeAnchorRowId, + focusedRowId: fileTreeFocusedRowId, + payload: { + selectedRowIds: Array.from(selectedFileTreeRowIds), + anchorRowId: fileTreeAnchorRowId, + focusedRowId: fileTreeFocusedRowId, + }, + }); + }; + + const getFileTreeRangeRowIds = (fromId, toId) => { + const fromIndex = visibleFileTreeRowIds.indexOf(fromId); + const toIndex = visibleFileTreeRowIds.indexOf(toId); + if (fromIndex < 0 || toIndex < 0) { + return [toId]; + } + const lo = Math.min(fromIndex, toIndex); + const hi = Math.max(fromIndex, toIndex); + return visibleFileTreeRowIds.slice(lo, hi + 1); + }; + + const commitFileTreeSelection = (nextSelection) => { + selectedFileTreeRowIds = nextSelection.selectedRowIds; + fileTreeAnchorRowId = nextSelection.anchorRowId; + fileTreeFocusedRowId = nextSelection.focusedRowId; + emitFileTreeSelectionChange(); + }; + + const selectFileTreeRow = (rowId, modifiers = {}) => { + const shiftKey = modifiers.shiftKey === true; + const metaKey = modifiers.metaKey === true; + const ctrlKey = modifiers.ctrlKey === true; + const toggleSelection = metaKey || ctrlKey; + + if (shiftKey) { + const anchor = fileTreeAnchorRowId || fileTreeFocusedRowId || rowId; + const nextSelection = toggleSelection ? new Set(selectedFileTreeRowIds) : new Set(); + getFileTreeRangeRowIds(anchor, rowId).forEach((id) => { + nextSelection.add(id); + }); + commitFileTreeSelection({ + selectedRowIds: nextSelection, + anchorRowId: fileTreeAnchorRowId || anchor, + focusedRowId: rowId, + }); + return; + } + + if (toggleSelection) { + const nextSelection = new Set(selectedFileTreeRowIds); + if (nextSelection.has(rowId)) { + nextSelection.delete(rowId); + } else { + nextSelection.add(rowId); + } + commitFileTreeSelection({ + selectedRowIds: nextSelection, + anchorRowId: rowId, + focusedRowId: rowId, + }); + return; + } + + commitFileTreeSelection({ + selectedRowIds: new Set([rowId]), + anchorRowId: rowId, + focusedRowId: rowId, + }); + }; + + const selectFileTreeContextRow = (rowId) => { + if (selectedFileTreeRowIds.has(rowId)) { + commitFileTreeSelection({ + selectedRowIds: new Set(selectedFileTreeRowIds), + anchorRowId: fileTreeAnchorRowId, + focusedRowId: rowId, + }); + return; + } + commitFileTreeSelection({ + selectedRowIds: new Set([rowId]), + anchorRowId: rowId, + focusedRowId: rowId, + }); + }; + + const clearFileTreeSelection = () => { + if (mode !== "filetree") return; + if ( + selectedFileTreeRowIds.size === 0 && + !fileTreeAnchorRowId && + !fileTreeFocusedRowId + ) { + return; + } + commitFileTreeSelection({ + selectedRowIds: new Set(), + anchorRowId: null, + focusedRowId: null, + }); + renderTree(); + }; + + const normalizeFileTreeSelectionForVisibleRows = () => { + if (mode !== "filetree") return; + const visibleSet = new Set(visibleFileTreeRowIds); + const nextSelectedRowIds = Array.from(selectedFileTreeRowIds).filter((rowId) => visibleSet.has(rowId)); + const nextAnchorRowId = + fileTreeAnchorRowId && visibleSet.has(fileTreeAnchorRowId) ? fileTreeAnchorRowId : null; + const nextFocusedRowId = + fileTreeFocusedRowId && visibleSet.has(fileTreeFocusedRowId) ? fileTreeFocusedRowId : null; + + if ( + nextSelectedRowIds.length === selectedFileTreeRowIds.size && + nextAnchorRowId === fileTreeAnchorRowId && + nextFocusedRowId === fileTreeFocusedRowId + ) { + return; + } + + commitFileTreeSelection({ + selectedRowIds: new Set(nextSelectedRowIds), + anchorRowId: nextAnchorRowId, + focusedRowId: nextFocusedRowId, + }); + }; + const scheduleRefresh = () => { window.setTimeout(() => { window.location.reload(); @@ -1142,6 +1531,30 @@ fn build_tree_shell_html( }; }; + const readFileTreeInternalDropPayload = (event) => { + const raw = + event.dataTransfer?.getData("application/x-mnote-file-tree") || + event.dataTransfer?.getData("text/plain") || + ""; + if (!raw) return null; + try { + const payload = JSON.parse(raw); + if ( + payload?.type !== "mnote-file-tree-dnd" || + payload.version !== 1 || + !Array.isArray(payload.rowIds) + ) { + return null; + } + const rowIds = payload.rowIds + .map((value) => normalizeText(value)) + .filter(Boolean); + return rowIds.length > 0 ? rowIds : null; + } catch { + return null; + } + }; + const handleRowKeyDown = (event, item) => { if (mode !== "page") return; const visible = getVisiblePageItems(); @@ -1506,163 +1919,148 @@ fn build_tree_shell_html( const renderFileTree = () => { appElement.innerHTML = ""; + visibleFileTreeRowIds = []; + clearFileTreeDropFeedback(); const fileRoot = document.createElement("div"); fileRoot.className = "tree-root"; fileRoot.setAttribute("role", "tree"); + fileRoot.dataset.dropTarget = String(activeFileTreeRootDrop); + fileRoot.addEventListener("mousedown", (event) => { + if (event.target !== event.currentTarget) return; + clearFileTreeSelection(); + }); - const appendAssetRow = (container, asset, depth) => { - const row = document.createElement("div"); - row.className = "tree-row"; - row.style.marginLeft = `${depth * 22}px`; - row.setAttribute("data-testid", "filetree-asset-row"); - row.dataset.assetId = asset.id; - row.dataset.rowId = `asset:${asset.id}`; - row.dataset.rowKind = "asset"; - row.dataset.shellMode = "filetree"; - row.dataset.selected = String(selectedFileTreeRowIds.has(`asset:${asset.id}`)); - row.tabIndex = 0; - row.setAttribute("role", "treeitem"); - row.setAttribute("aria-level", String(depth + 1)); - row.addEventListener("click", () => { - selectedFileTreeRowIds = new Set([`asset:${asset.id}`]); - renderTree(); - }); - row.addEventListener("contextmenu", (event) => { - event.preventDefault(); - selectedFileTreeRowIds = new Set([`asset:${asset.id}`]); - renderTree(); - openFileTreeContextMenu({ - documentId: asset.documentId, - assetId: asset.id, - rowId: `asset:${asset.id}`, - rowKind: "asset", - clientX: event.clientX, - clientY: event.clientY, - }); - }); - - const spacer = document.createElement("div"); - spacer.className = "tree-spacer"; - row.appendChild(spacer); - row.appendChild( - createKindBadge( - asset.assetType === "mindmap" - ? "mindmap" - : asset.assetType === "luckysheet" - ? "table" - : "file", - ), - ); - - const button = document.createElement("button"); - button.type = "button"; - button.className = "tree-link"; - button.setAttribute("data-testid", "filetree-asset-open"); - button.addEventListener("click", () => { - setLastAction(`准备打开附件 ${asset.id}`); - postToHost("tree.asset.open", { - assetId: asset.id, - documentId: asset.documentId, - target: { documentId: asset.documentId }, - payload: { documentId: asset.documentId, assetId: asset.id }, - }); - }); - const title = document.createElement("span"); - title.className = "tree-link-title"; - title.textContent = asset.fileName; - const meta = document.createElement("span"); - meta.className = "tree-link-meta"; - meta.textContent = `${asset.assetType} · ${asset.id}`; - button.appendChild(title); - button.appendChild(meta); - row.appendChild(button); - const actions = document.createElement("div"); - actions.className = "tree-actions"; - actions.appendChild( - createActionButton( - ICONS.more, - "filetree-action-menu", - `打开 ${asset.fileName} 的更多操作`, - (button) => { - const center = getElementCenter(button); - openFileTreeContextMenu({ - documentId: asset.documentId, - assetId: asset.id, - rowId: `asset:${asset.id}`, - rowKind: "asset", - clientX: center.x, - clientY: center.y, - }); - }, - false, - ), - ); - row.appendChild(actions); - container.appendChild(row); - - const childIds = Array.isArray(mindmapAssetChildren[asset.id]) ? mindmapAssetChildren[asset.id] : []; - if (childIds.length === 0) return; - childIds.forEach((childId) => { - const child = mediaAssets.find((entry) => normalizeText(entry?.id) === childId); - if (!child) return; - appendAssetRow( - container, - { - id: normalizeText(child?.id), - documentId: normalizeText(child?.document_id), - assetType: normalizeText(child?.asset_type, "file"), - fileName: normalizeText(child?.file_name, "附件"), - storagePath: normalizeText(child?.storage_path), - }, - depth + 1, - ); + const openFileTreeItem = (item) => { + const documentId = getFileTreeRowDocumentId(item); + const assetId = getFileTreeRowAssetId(item); + if (item.rowKind === "document" || item.rowKind === "index") { + handleNavigate(documentId || item.nodeId); + return; + } + setLastAction(`准备打开资源 ${assetId || item.rowId}`); + postToHost("tree.asset.open", { + documentId: documentId || null, + assetId: assetId || null, + target: { documentId: documentId || null }, + payload: { + documentId: documentId || null, + assetId: assetId || null, + rowId: item.rowId, + rowKind: item.rowKind, + }, }); }; - const appendDoc = (item, depth) => { - const wrapper = document.createElement("div"); - wrapper.className = "tree-node"; - wrapper.dataset.nodeId = item.nodeId; + const postFileTreeDropToHost = (type, target, extra = {}) => { + postToHost(type, { + workspaceId, + rowId: target.rowId, + rowKind: target.rowKind, + targetRowId: target.rowId, + targetRowKind: target.rowKind, + documentId: target.documentId, + assetId: target.assetId, + ...extra, + payload: { + workspaceId, + rowId: target.rowId, + rowKind: target.rowKind, + targetRowId: target.rowId, + targetRowKind: target.rowKind, + documentId: target.documentId, + assetId: target.assetId, + ...extra, + }, + }); + }; + + const attachFileTreeDragSource = (row, item) => { + row.draggable = true; + row.addEventListener("dragstart", (event) => { + const rowIds = resolveFileTreeDraggedRowIds(item.rowId); + draggingFileTreeRowIds = rowIds; + if (event.dataTransfer) { + const payload = JSON.stringify({ + type: "mnote-file-tree-dnd", + version: 1, + rowIds, + }); + event.dataTransfer.effectAllowed = "copyMove"; + event.dataTransfer.setData(FILETREE_DRAG_MIME, payload); + event.dataTransfer.setData("application/x-mnote-file-tree", payload); + event.dataTransfer.setData("text/plain", payload); + } + setLastAction(`开始拖拽 ${rowIds.length} 个文件树节点`); + }); + row.addEventListener("dragend", () => { + draggingFileTreeRowIds = []; + clearFileTreeDropFeedback(); + }); + }; + + const appendFileTreeRow = (container, item) => { + const children = getSiblings(item.nodeId); + const hasBranches = canExpandFileTreeRow(item) && children.length > 0; + const documentId = getFileTreeRowDocumentId(item) || null; + const assetId = getFileTreeRowAssetId(item) || null; const row = document.createElement("div"); row.className = "tree-row"; - row.style.marginLeft = `${depth * 22}px`; - row.dataset.active = String(item.nodeId === activeDocumentId); - row.setAttribute("data-testid", "filetree-doc-row"); - row.dataset.rowId = `doc:${item.nodeId}`; - row.dataset.rowKind = "doc"; + row.style.marginLeft = `${item.depth * 22}px`; + row.dataset.active = String(item.rowKind === "document" && documentId === activeDocumentId); + row.dataset.nodeId = item.nodeId; + row.dataset.rowId = item.rowId; + row.dataset.rowKind = item.rowKind; + row.dataset.documentId = documentId || ""; + row.dataset.assetId = assetId || ""; row.dataset.shellMode = "filetree"; - row.dataset.selected = String(selectedFileTreeRowIds.has(`doc:${item.nodeId}`)); + row.dataset.selected = String(selectedFileTreeRowIds.has(item.rowId)); + row.dataset.dropTarget = String(activeFileTreeDropRowId === item.rowId); + row.setAttribute( + "data-testid", + item.rowKind === "document" + ? "filetree-doc-row" + : item.rowKind === "index" + ? "filetree-index-row" + : "filetree-asset-row", + ); row.tabIndex = 0; row.setAttribute("role", "treeitem"); - row.setAttribute("aria-level", String(depth + 1)); - const children = getSiblings(item.nodeId); - const assets = assetsByDocId.get(item.nodeId) || []; - const hasBranches = children.length > 0 || assets.length > 0; + row.setAttribute("aria-level", String(item.depth + 1)); row.setAttribute("aria-expanded", hasBranches ? String(expanded.has(item.nodeId)) : "false"); - row.addEventListener("click", () => { - selectedFileTreeRowIds = new Set([`doc:${item.nodeId}`]); + visibleFileTreeRowIds.push(item.rowId); + + row.addEventListener("click", (event) => { + selectFileTreeRow(item.rowId, event); renderTree(); }); + row.addEventListener("dblclick", () => { + openFileTreeItem(item); + }); row.addEventListener("contextmenu", (event) => { event.preventDefault(); - selectedFileTreeRowIds = new Set([`doc:${item.nodeId}`]); + selectFileTreeContextRow(item.rowId); renderTree(); openFileTreeContextMenu({ - documentId: item.nodeId, - rowId: `doc:${item.nodeId}`, - rowKind: "doc", + documentId, + assetId, + rowId: item.rowId, + rowKind: item.rowKind, clientX: event.clientX, clientY: event.clientY, }); }); + attachFileTreeDragSource(row, item); if (hasBranches) { const toggleButton = document.createElement("button"); toggleButton.type = "button"; toggleButton.className = "tree-toggle"; - toggleButton.setAttribute("aria-label", `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`); + toggleButton.setAttribute( + "aria-label", + `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`, + ); toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸"; toggleButton.addEventListener("click", (event) => { event.stopPropagation(); @@ -1675,21 +2073,27 @@ fn build_tree_shell_html( row.appendChild(spacer); } - row.appendChild(createKindBadge("page")); + row.appendChild(createKindBadge(getFileTreeRowIconKind(item))); + const linkButton = document.createElement("button"); linkButton.type = "button"; linkButton.className = "tree-link"; - linkButton.setAttribute("data-testid", "filetree-doc-open"); - linkButton.addEventListener("click", () => handleNavigate(item.nodeId)); + if (item.rowKind === "document") { + linkButton.setAttribute("data-testid", "filetree-doc-open"); + } else if (item.rowKind === "asset" || item.rowKind === "asset_folder") { + linkButton.setAttribute("data-testid", "filetree-asset-open"); + } + linkButton.addEventListener("click", () => openFileTreeItem(item)); const title = document.createElement("span"); title.className = "tree-link-title"; title.textContent = item.title; const meta = document.createElement("span"); meta.className = "tree-link-meta"; - meta.textContent = `${item.nodeId} · 页面`; + meta.textContent = getFileTreeRowMetaLabel(item); linkButton.appendChild(title); linkButton.appendChild(meta); row.appendChild(linkButton); + const actions = document.createElement("div"); actions.className = "tree-actions"; actions.appendChild( @@ -1700,9 +2104,10 @@ fn build_tree_shell_html( (button) => { const center = getElementCenter(button); openFileTreeContextMenu({ - documentId: item.nodeId, - rowId: `doc:${item.nodeId}`, - rowKind: "doc", + documentId, + assetId, + rowId: item.rowId, + rowKind: item.rowKind, clientX: center.x, clientY: center.y, }); @@ -1711,90 +2116,71 @@ fn build_tree_shell_html( ), ); row.appendChild(actions); - wrapper.appendChild(row); + container.appendChild(row); - const indexRow = document.createElement("div"); - indexRow.className = "tree-row"; - indexRow.style.marginLeft = `${(depth + 1) * 22}px`; - indexRow.setAttribute("data-testid", "filetree-index-row"); - indexRow.dataset.rowId = `index:${item.nodeId}`; - indexRow.dataset.rowKind = "index"; - indexRow.dataset.shellMode = "filetree"; - indexRow.dataset.selected = String(selectedFileTreeRowIds.has(`index:${item.nodeId}`)); - indexRow.tabIndex = 0; - indexRow.setAttribute("role", "treeitem"); - indexRow.setAttribute("aria-level", String(depth + 2)); - indexRow.addEventListener("click", () => { - selectedFileTreeRowIds = new Set([`index:${item.nodeId}`]); - renderTree(); - }); - indexRow.addEventListener("contextmenu", (event) => { - event.preventDefault(); - selectedFileTreeRowIds = new Set([`index:${item.nodeId}`]); - renderTree(); - openFileTreeContextMenu({ - documentId: item.nodeId, - rowId: `index:${item.nodeId}`, - rowKind: "index", - clientX: event.clientX, - clientY: event.clientY, - }); - }); - const indexSpacer = document.createElement("div"); - indexSpacer.className = "tree-spacer"; - indexRow.appendChild(indexSpacer); - indexRow.appendChild(createKindBadge("index")); - const indexLink = document.createElement("button"); - indexLink.type = "button"; - indexLink.className = "tree-link"; - indexLink.addEventListener("click", () => handleNavigate(item.nodeId)); - const indexTitle = document.createElement("span"); - indexTitle.className = "tree-link-title"; - indexTitle.textContent = "index.md"; - const indexMeta = document.createElement("span"); - indexMeta.className = "tree-link-meta"; - indexMeta.textContent = "页面正文"; - indexLink.appendChild(indexTitle); - indexLink.appendChild(indexMeta); - indexRow.appendChild(indexLink); - const indexActions = document.createElement("div"); - indexActions.className = "tree-actions"; - indexActions.appendChild( - createActionButton( - ICONS.more, - "filetree-action-menu", - `打开 ${item.title} 正文的更多操作`, - (button) => { - const center = getElementCenter(button); - openFileTreeContextMenu({ - documentId: item.nodeId, - rowId: `index:${item.nodeId}`, - rowKind: "index", - clientX: center.x, - clientY: center.y, - }); - }, - false, - ), - ); - indexRow.appendChild(indexActions); - if (expanded.has(item.nodeId) || !hasBranches) { - wrapper.appendChild(indexRow); - assets.forEach((asset) => appendAssetRow(wrapper, asset, depth + 1)); - children.forEach((child) => appendDoc(child, depth + 1)); - } - fileRoot.appendChild(wrapper); + if (!hasBranches || !expanded.has(item.nodeId)) return; + children.forEach((child) => appendFileTreeRow(container, child)); }; + fileRoot.addEventListener("dragover", (event) => { + const internalRowIds = readFileTreeInternalDropPayload(event); + const files = Array.from(event.dataTransfer?.files || []); + if (!internalRowIds && files.length === 0) { + return; + } + event.preventDefault(); + const target = getFileTreeDropTargetFromElement(event.target); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = + files.length > 0 || event.altKey ? "copy" : "move"; + } + setFileTreeDropFeedback(target); + }); + fileRoot.addEventListener("dragleave", (event) => { + const relatedTarget = + event.relatedTarget instanceof Node ? event.relatedTarget : null; + if (relatedTarget && fileRoot.contains(relatedTarget)) { + return; + } + clearFileTreeDropFeedback(); + }); + fileRoot.addEventListener("drop", (event) => { + const internalRowIds = readFileTreeInternalDropPayload(event); + const files = Array.from(event.dataTransfer?.files || []); + if (!internalRowIds && files.length === 0) { + return; + } + event.preventDefault(); + const target = getFileTreeDropTargetFromElement(event.target); + if (files.length > 0) { + setLastAction(`已发送 ${files.length} 个外部文件到宿主`); + postFileTreeDropToHost("tree.filetree.external-drop", target, { + files, + }); + } else { + setLastAction( + event.altKey + ? `已发送复制拖放到 ${target.rowId || "根目录"}` + : `已发送移动拖放到 ${target.rowId || "根目录"}`, + ); + postFileTreeDropToHost("tree.filetree.internal-drop", target, { + rowIds: internalRowIds, + copy: event.altKey === true, + }); + } + draggingFileTreeRowIds = []; + clearFileTreeDropFeedback(); + }); + if (roots.length === 0) { const empty = document.createElement("div"); empty.className = "tree-empty"; empty.textContent = "当前 file tree 没有可渲染的页面。"; - appElement.appendChild(empty); - return; + fileRoot.appendChild(empty); + } else { + roots.forEach((item) => appendFileTreeRow(fileRoot, item)); } - - roots.forEach((item) => appendDoc(item, 0)); + normalizeFileTreeSelectionForVisibleRows(); appElement.appendChild(fileRoot); }; @@ -1871,6 +2257,9 @@ fn build_tree_shell_html( }; renderTree(); + if (mode === "filetree") { + emitFileTreeSelectionChange(); + } setStatus( mode === "picker" ? "Tree picker 已就绪,可以展开目录并选择目标页面。" @@ -2219,7 +2608,7 @@ mod tests { convex_url: None, convex_admin_key: None, allow_dev_fixtures: true, - query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}]}}"#.into()), + query_fixtures_json: Some(r#"{"sidebar:datasetList":{"active_workspace_id":"ws_demo","documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"luckysheet","file_name":"预算.luckysheet","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}}}"#.into()), mutation_fixtures_json: Some(r#"{"documents:createWithParentReference":{"ok":true,"id":"page_new","updated_at":"2026-04-17T00:00:00Z"},"documents:updateTitle":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"},"documents:move":{"ok":true,"updated_at":"2026-04-17T00:00:00Z"}}"#.into()), dev_user_id: "dev-user".into(), dev_user_name: "开发用户".into(), @@ -2281,6 +2670,7 @@ mod tests { assert!(html.contains("\"allowRootPick\":true")); assert!(html.contains("\"excludeIds\":[\"page_child\"]")); assert!(html.contains("tree.pick.root")); + assert!(html.contains("__MNOTE_TREE_SHELL_OVERRIDE__")); } #[tokio::test] @@ -2302,6 +2692,12 @@ mod tests { let html = String::from_utf8(body.to_vec()).expect("utf8"); assert!(html.contains("filetree-doc-row")); assert!(html.contains("\"mediaAssets\"")); + assert!(html.contains("tree.filetree.selection.changed")); + assert!(html.contains("tree.filetree.internal-drop")); + assert!(html.contains("tree.filetree.external-drop")); + assert!(html.contains("\"rowKind\":\"asset_folder\"")); + assert!(html.contains("\"resourceMeta\"")); + assert!(html.contains("dragover")); } #[tokio::test] diff --git a/rust/mnote-web-dev-codex.err b/rust/mnote-web-dev-codex.err new file mode 100644 index 00000000..f5d6300a --- /dev/null +++ b/rust/mnote-web-dev-codex.err @@ -0,0 +1,7 @@ + Blocking waiting for file lock on package cache + Blocking waiting for file lock on package cache + Blocking waiting for file lock on package cache + Compiling mnote-web v0.1.0 (/mnt/Data1T/mnote/rust/crates/mnote-web) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.69s + Running `target/debug/mnote-web` +2026-04-23T16:14:28.636856Z INFO mnote-web 最小骨架已启动 bind_addr=127.0.0.1:3104 diff --git a/wolai-backend/.data/openai-agents-sessions.sqlite3 b/wolai-backend/.data/openai-agents-sessions.sqlite3 new file mode 100644 index 00000000..d3e4bb84 Binary files /dev/null and b/wolai-backend/.data/openai-agents-sessions.sqlite3 differ diff --git a/wolai-backend/backend-dev-codex.err b/wolai-backend/backend-dev-codex.err new file mode 100644 index 00000000..87d235b7 --- /dev/null +++ b/wolai-backend/backend-dev-codex.err @@ -0,0 +1,17 @@ +INFO: Will watch for changes in these directories: ['/mnt/Data1T/mnote/wolai-backend'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [1431406] using WatchFiles +INFO: Started server process [1431452] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: 127.0.0.1:47792 - "HEAD / HTTP/1.1" 405 Method Not Allowed +INFO: 127.0.0.1:37784 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:36884 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:48348 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:53220 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:48870 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:49756 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:49756 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:49732 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:49732 - "GET /health HTTP/1.1" 200 OK +INFO: 127.0.0.1:49732 - "GET /health HTTP/1.1" 200 OK diff --git a/wolai-backend/backend-dev-codex.out b/wolai-backend/backend-dev-codex.out new file mode 100644 index 00000000..e69de29b diff --git a/wolai-frontend/next-dev-codex.err b/wolai-frontend/next-dev-codex.err new file mode 100644 index 00000000..39a2be96 --- /dev/null +++ b/wolai-frontend/next-dev-codex.err @@ -0,0 +1,445 @@ + +> wolai-frontend@0.1.0 dev /mnt/Data1T/mnote/wolai-frontend +> node scripts/dev-server.js -p 3000 + +[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D` + ⚠ The "middleware" file convention is deprecated. Please use "proxy" instead. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy +[dev-server] ready http://0.0.0.0:3000 (ONLYOFFICE ws via /onlyoffice-server -> http://127.0.0.1:8082; Convex ws via /convex -> http://127.0.0.1:3210) + GET / 307 in 1911ms (compile: 1670ms, proxy.ts: 30ms, render: 211ms) + ○ Compiling /documents/[id] ... + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 4.1s (compile: 3.6s, proxy.ts: 9ms, render: 536ms) + ⚠ Cross origin request detected from 127.0.0.1 to /_next/* resource. In a future major version of Next.js, you will need to explicitly configure "allowedDevOrigins" in next.config to allow this. +Read more: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 366ms (compile: 319ms, proxy.ts: 8ms, render: 40ms) + GET /api/backend/health 200 in 98ms (compile: 81ms, proxy.ts: 11ms, render: 6ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 924ms (compile: 186ms, proxy.ts: 13ms, render: 725ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 146ms (compile: 122ms, proxy.ts: 8ms, render: 16ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 719ms (compile: 695ms, proxy.ts: 14ms, render: 9ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 724ms (compile: 698ms, proxy.ts: 15ms, render: 11ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 97ms (compile: 86ms, proxy.ts: 6ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 47ms (compile: 21ms, proxy.ts: 13ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 50ms (compile: 19ms, proxy.ts: 14ms, render: 17ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 49ms (compile: 21ms, proxy.ts: 14ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 49ms (compile: 19ms, proxy.ts: 17ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 52ms (compile: 20ms, proxy.ts: 17ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 55ms (compile: 20ms, proxy.ts: 18ms, render: 16ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 51ms (compile: 22ms, proxy.ts: 17ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 52ms (compile: 23ms, proxy.ts: 15ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 53ms (compile: 21ms, proxy.ts: 18ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 50ms (compile: 22ms, proxy.ts: 15ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 52ms (compile: 17ms, proxy.ts: 20ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 52ms (compile: 19ms, proxy.ts: 19ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 47ms (compile: 18ms, proxy.ts: 16ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 46ms (compile: 19ms, proxy.ts: 15ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 46ms (compile: 19ms, proxy.ts: 16ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 48ms (compile: 20ms, proxy.ts: 17ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 43ms (compile: 20ms, proxy.ts: 14ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 43ms (compile: 20ms, proxy.ts: 12ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 45ms (compile: 16ms, proxy.ts: 13ms, render: 16ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 45ms (compile: 17ms, proxy.ts: 13ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 44ms (compile: 17ms, proxy.ts: 14ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 42ms (compile: 20ms, proxy.ts: 13ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 43ms (compile: 19ms, proxy.ts: 16ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 41ms (compile: 19ms, proxy.ts: 15ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 36ms (compile: 15ms, proxy.ts: 10ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 36ms (compile: 14ms, proxy.ts: 11ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 36ms (compile: 15ms, proxy.ts: 11ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 38ms (compile: 16ms, proxy.ts: 14ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 37ms (compile: 16ms, proxy.ts: 14ms, render: 7ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 30ms (compile: 3ms, proxy.ts: 8ms, render: 19ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 60ms (compile: 4ms, proxy.ts: 10ms, render: 46ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 41ms (compile: 4ms, proxy.ts: 20ms, render: 18ms) + GET /auth 200 in 441ms (compile: 376ms, proxy.ts: 4ms, render: 60ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 511ms (compile: 114ms, proxy.ts: 19ms, render: 378ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 29ms (compile: 3ms, proxy.ts: 11ms, render: 15ms) + GET /api/backend/health 200 in 12ms (compile: 1739µs, proxy.ts: 7ms, render: 4ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 111ms (compile: 89ms, proxy.ts: 11ms, render: 11ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 110ms (compile: 91ms, proxy.ts: 10ms, render: 9ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 11ms (compile: 3ms, proxy.ts: 6ms, render: 2ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 39ms (compile: 15ms, proxy.ts: 12ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 41ms (compile: 13ms, proxy.ts: 12ms, render: 16ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 40ms (compile: 15ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 38ms (compile: 18ms, proxy.ts: 13ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 38ms (compile: 19ms, proxy.ts: 14ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 36ms (compile: 14ms, proxy.ts: 10ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 35ms (compile: 15ms, proxy.ts: 9ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 34ms (compile: 15ms, proxy.ts: 10ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 35ms (compile: 15ms, proxy.ts: 12ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 35ms (compile: 16ms, proxy.ts: 13ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 57ms (compile: 17ms, proxy.ts: 13ms, render: 26ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 56ms (compile: 15ms, proxy.ts: 16ms, render: 25ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 56ms (compile: 16ms, proxy.ts: 16ms, render: 23ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 56ms (compile: 17ms, proxy.ts: 17ms, render: 22ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 56ms (compile: 30ms, proxy.ts: 20ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 33ms (compile: 12ms, proxy.ts: 11ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 33ms (compile: 13ms, proxy.ts: 11ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 35ms (compile: 14ms, proxy.ts: 11ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 37ms (compile: 13ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 39ms (compile: 17ms, proxy.ts: 13ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 34ms (compile: 14ms, proxy.ts: 10ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 34ms (compile: 14ms, proxy.ts: 11ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 35ms (compile: 15ms, proxy.ts: 12ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 39ms (compile: 15ms, proxy.ts: 12ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 37ms (compile: 17ms, proxy.ts: 10ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 28ms (compile: 10ms, proxy.ts: 10ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 31ms (compile: 11ms, proxy.ts: 10ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 30ms (compile: 11ms, proxy.ts: 12ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 26ms (compile: 12ms, proxy.ts: 9ms, render: 5ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 719ms (compile: 1610µs, proxy.ts: 6ms, render: 711ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 76ms (compile: 3ms, proxy.ts: 9ms, render: 64ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 51ms (compile: 3ms, proxy.ts: 9ms, render: 40ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 17ms (compile: 1686µs, proxy.ts: 7ms, render: 8ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 37ms (compile: 3ms, proxy.ts: 17ms, render: 17ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 483ms (compile: 111ms, proxy.ts: 18ms, render: 354ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 28ms (compile: 2ms, proxy.ts: 6ms, render: 20ms) + GET /api/backend/health 200 in 14ms (compile: 2ms, proxy.ts: 8ms, render: 3ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 113ms (compile: 90ms, proxy.ts: 8ms, render: 15ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 112ms (compile: 98ms, proxy.ts: 7ms, render: 7ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 18ms (compile: 3ms, proxy.ts: 12ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 41ms (compile: 19ms, proxy.ts: 12ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 44ms (compile: 18ms, proxy.ts: 12ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 41ms (compile: 18ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 41ms (compile: 15ms, proxy.ts: 18ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 41ms (compile: 16ms, proxy.ts: 19ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 43ms (compile: 20ms, proxy.ts: 10ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 42ms (compile: 19ms, proxy.ts: 12ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 42ms (compile: 21ms, proxy.ts: 12ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 43ms (compile: 20ms, proxy.ts: 14ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 48ms (compile: 21ms, proxy.ts: 15ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 34ms (compile: 13ms, proxy.ts: 10ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 34ms (compile: 13ms, proxy.ts: 11ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 40ms (compile: 13ms, proxy.ts: 13ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 42ms (compile: 14ms, proxy.ts: 14ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 39ms (compile: 16ms, proxy.ts: 11ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 38ms (compile: 13ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 38ms (compile: 13ms, proxy.ts: 15ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 34ms (compile: 13ms, proxy.ts: 13ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 33ms (compile: 13ms, proxy.ts: 12ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 31ms (compile: 13ms, proxy.ts: 11ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 42ms (compile: 14ms, proxy.ts: 16ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 42ms (compile: 15ms, proxy.ts: 15ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 42ms (compile: 16ms, proxy.ts: 15ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 62ms (compile: 15ms, proxy.ts: 19ms, render: 28ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 61ms (compile: 16ms, proxy.ts: 16ms, render: 29ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 28ms (compile: 9ms, proxy.ts: 11ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 28ms (compile: 10ms, proxy.ts: 11ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 29ms (compile: 10ms, proxy.ts: 11ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 25ms (compile: 10ms, proxy.ts: 9ms, render: 5ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 739ms (compile: 1795µs, proxy.ts: 7ms, render: 731ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 80ms (compile: 2ms, proxy.ts: 7ms, render: 71ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 63ms (compile: 2ms, proxy.ts: 7ms, render: 54ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 22ms (compile: 1535µs, proxy.ts: 6ms, render: 15ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 29ms (compile: 3ms, proxy.ts: 8ms, render: 18ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 481ms (compile: 111ms, proxy.ts: 19ms, render: 352ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 27ms (compile: 2ms, proxy.ts: 7ms, render: 18ms) + GET /api/backend/health 200 in 14ms (compile: 2ms, proxy.ts: 8ms, render: 3ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 115ms (compile: 93ms, proxy.ts: 7ms, render: 15ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 115ms (compile: 102ms, proxy.ts: 7ms, render: 6ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 13ms (compile: 3ms, proxy.ts: 7ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 42ms (compile: 16ms, proxy.ts: 14ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 37ms (compile: 16ms, proxy.ts: 11ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 43ms (compile: 17ms, proxy.ts: 17ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 39ms (compile: 17ms, proxy.ts: 13ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 45ms (compile: 17ms, proxy.ts: 13ms, render: 15ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 36ms (compile: 14ms, proxy.ts: 11ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 35ms (compile: 14ms, proxy.ts: 12ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 39ms (compile: 15ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 40ms (compile: 16ms, proxy.ts: 14ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 43ms (compile: 17ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 45ms (compile: 21ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 45ms (compile: 20ms, proxy.ts: 16ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 43ms (compile: 21ms, proxy.ts: 14ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 50ms (compile: 16ms, proxy.ts: 20ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 45ms (compile: 17ms, proxy.ts: 15ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 40ms (compile: 14ms, proxy.ts: 14ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 40ms (compile: 15ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 42ms (compile: 15ms, proxy.ts: 14ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 43ms (compile: 19ms, proxy.ts: 11ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 40ms (compile: 21ms, proxy.ts: 8ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 40ms (compile: 17ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 42ms (compile: 17ms, proxy.ts: 16ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 45ms (compile: 17ms, proxy.ts: 17ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 56ms (compile: 17ms, proxy.ts: 10ms, render: 29ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 57ms (compile: 18ms, proxy.ts: 9ms, render: 30ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 50ms (compile: 9ms, proxy.ts: 33ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 49ms (compile: 10ms, proxy.ts: 31ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 32ms (compile: 11ms, proxy.ts: 14ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 25ms (compile: 11ms, proxy.ts: 10ms, render: 5ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 738ms (compile: 1573µs, proxy.ts: 7ms, render: 730ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 62ms (compile: 3ms, proxy.ts: 7ms, render: 53ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 49ms (compile: 2ms, proxy.ts: 6ms, render: 40ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 17ms (compile: 2ms, proxy.ts: 7ms, render: 8ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 40ms (compile: 3ms, proxy.ts: 16ms, render: 20ms) + ✓ Compiled in 80ms + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 38ms (compile: 2ms, proxy.ts: 18ms, render: 18ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 24ms (compile: 1593µs, proxy.ts: 7ms, render: 16ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 747ms (compile: 183ms, proxy.ts: 17ms, render: 547ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 26ms (compile: 2ms, proxy.ts: 8ms, render: 16ms) + GET /api/backend/health 200 in 14ms (compile: 2ms, proxy.ts: 8ms, render: 4ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 114ms (compile: 95ms, proxy.ts: 9ms, render: 10ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 112ms (compile: 98ms, proxy.ts: 10ms, render: 5ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 12ms (compile: 3ms, proxy.ts: 6ms, render: 2ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 47ms (compile: 18ms, proxy.ts: 12ms, render: 17ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 49ms (compile: 16ms, proxy.ts: 11ms, render: 22ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 44ms (compile: 19ms, proxy.ts: 11ms, render: 15ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 48ms (compile: 21ms, proxy.ts: 14ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 46ms (compile: 22ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 36ms (compile: 13ms, proxy.ts: 12ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 34ms (compile: 14ms, proxy.ts: 11ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 34ms (compile: 15ms, proxy.ts: 11ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 35ms (compile: 14ms, proxy.ts: 14ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 33ms (compile: 14ms, proxy.ts: 13ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 39ms (compile: 18ms, proxy.ts: 10ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 38ms (compile: 17ms, proxy.ts: 10ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 38ms (compile: 15ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 38ms (compile: 14ms, proxy.ts: 16ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 39ms (compile: 15ms, proxy.ts: 17ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 39ms (compile: 13ms, proxy.ts: 12ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 39ms (compile: 14ms, proxy.ts: 11ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 39ms (compile: 15ms, proxy.ts: 12ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 39ms (compile: 14ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 42ms (compile: 17ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 36ms (compile: 12ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 36ms (compile: 12ms, proxy.ts: 14ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 36ms (compile: 12ms, proxy.ts: 15ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 36ms (compile: 12ms, proxy.ts: 17ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 32ms (compile: 13ms, proxy.ts: 13ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 32ms (compile: 10ms, proxy.ts: 12ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 32ms (compile: 11ms, proxy.ts: 12ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 30ms (compile: 10ms, proxy.ts: 12ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 31ms (compile: 11ms, proxy.ts: 13ms, render: 6ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 726ms (compile: 1595µs, proxy.ts: 7ms, render: 718ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 27ms (compile: 2ms, proxy.ts: 6ms, render: 18ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 81ms (compile: 2ms, proxy.ts: 10ms, render: 69ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 16ms (compile: 1736µs, proxy.ts: 7ms, render: 7ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 40ms (compile: 2ms, proxy.ts: 18ms, render: 19ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 509ms (compile: 114ms, proxy.ts: 39ms, render: 357ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 21ms (compile: 1697µs, proxy.ts: 7ms, render: 12ms) + GET /api/backend/health 200 in 11ms (compile: 1606µs, proxy.ts: 5ms, render: 4ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 122ms (compile: 99ms, proxy.ts: 14ms, render: 9ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 115ms (compile: 102ms, proxy.ts: 9ms, render: 3ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 13ms (compile: 3ms, proxy.ts: 6ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 38ms (compile: 16ms, proxy.ts: 14ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 42ms (compile: 17ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 42ms (compile: 17ms, proxy.ts: 16ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 40ms (compile: 16ms, proxy.ts: 16ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 41ms (compile: 16ms, proxy.ts: 19ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 29ms (compile: 15ms, proxy.ts: 6ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 38ms (compile: 19ms, proxy.ts: 11ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 38ms (compile: 20ms, proxy.ts: 11ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 38ms (compile: 18ms, proxy.ts: 14ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 38ms (compile: 17ms, proxy.ts: 17ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 35ms (compile: 12ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 35ms (compile: 13ms, proxy.ts: 13ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 35ms (compile: 14ms, proxy.ts: 13ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 35ms (compile: 14ms, proxy.ts: 13ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 35ms (compile: 14ms, proxy.ts: 16ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 39ms (compile: 11ms, proxy.ts: 18ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 39ms (compile: 12ms, proxy.ts: 18ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 39ms (compile: 12ms, proxy.ts: 18ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 39ms (compile: 13ms, proxy.ts: 18ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 40ms (compile: 14ms, proxy.ts: 18ms, render: 7ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 703ms (compile: 1547µs, proxy.ts: 6ms, render: 695ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 49ms (compile: 20ms, proxy.ts: 15ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 49ms (compile: 21ms, proxy.ts: 15ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 49ms (compile: 22ms, proxy.ts: 16ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 49ms (compile: 23ms, proxy.ts: 16ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 49ms (compile: 23ms, proxy.ts: 17ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 50ms (compile: 18ms, proxy.ts: 24ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 111ms (compile: 95ms, proxy.ts: 8ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 112ms (compile: 97ms, proxy.ts: 8ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 112ms (compile: 98ms, proxy.ts: 8ms, render: 6ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 62ms (compile: 3ms, proxy.ts: 6ms, render: 53ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 68ms (compile: 3ms, proxy.ts: 6ms, render: 59ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 23ms (compile: 1505µs, proxy.ts: 7ms, render: 15ms) + GET / 307 in 85ms (compile: 2ms, proxy.ts: 7ms, render: 77ms) + GET /documents/8337ceec-e236-41d7-a2eb-86838f46c858 200 in 451ms (compile: 118ms, proxy.ts: 6ms, render: 327ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 19ms (compile: 1817µs, proxy.ts: 6ms, render: 12ms) + GET /api/backend/health 200 in 11ms (compile: 1466µs, proxy.ts: 6ms, render: 3ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 290ms (compile: 2ms, proxy.ts: 4ms, render: 284ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 107ms (compile: 91ms, proxy.ts: 10ms, render: 6ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 107ms (compile: 93ms, proxy.ts: 10ms, render: 5ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 13ms (compile: 3ms, proxy.ts: 7ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 23ms (compile: 9ms, proxy.ts: 9ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 25ms (compile: 8ms, proxy.ts: 9ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 24ms (compile: 9ms, proxy.ts: 10ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 25ms (compile: 7ms, proxy.ts: 12ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 23ms (compile: 7ms, proxy.ts: 11ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 18ms (compile: 5ms, proxy.ts: 11ms, render: 2ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 21ms (compile: 6ms, proxy.ts: 8ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 21ms (compile: 7ms, proxy.ts: 8ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 21ms (compile: 8ms, proxy.ts: 8ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 24ms (compile: 8ms, proxy.ts: 10ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 24ms (compile: 10ms, proxy.ts: 9ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 23ms (compile: 10ms, proxy.ts: 9ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 21ms (compile: 7ms, proxy.ts: 9ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 21ms (compile: 7ms, proxy.ts: 9ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 21ms (compile: 7ms, proxy.ts: 10ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 117ms (compile: 96ms, proxy.ts: 9ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 117ms (compile: 97ms, proxy.ts: 9ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 118ms (compile: 98ms, proxy.ts: 9ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 46ms (compile: 6ms, proxy.ts: 33ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 46ms (compile: 7ms, proxy.ts: 33ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 44ms (compile: 8ms, proxy.ts: 32ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 21ms (compile: 7ms, proxy.ts: 7ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 21ms (compile: 8ms, proxy.ts: 8ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 21ms (compile: 8ms, proxy.ts: 9ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 19ms (compile: 7ms, proxy.ts: 7ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 19ms (compile: 7ms, proxy.ts: 7ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 20ms (compile: 8ms, proxy.ts: 8ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 16ms (compile: 5ms, proxy.ts: 7ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 16ms (compile: 5ms, proxy.ts: 9ms, render: 3ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 57ms (compile: 2ms, proxy.ts: 6ms, render: 49ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 54ms (compile: 3ms, proxy.ts: 6ms, render: 45ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 17ms (compile: 1539µs, proxy.ts: 7ms, render: 8ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=8337ceec-e236-41d7-a2eb-86838f46c858 200 in 23ms (compile: 1655µs, proxy.ts: 7ms, render: 14ms) + GET /documents/ced55414-84b5-4d88-8af6-9a2d54f42b36 200 in 283ms (compile: 106ms, proxy.ts: 7ms, render: 170ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=ced55414-84b5-4d88-8af6-9a2d54f42b36 200 in 34ms (compile: 2ms, proxy.ts: 8ms, render: 24ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 105ms (compile: 89ms, proxy.ts: 5ms, render: 10ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 104ms (compile: 95ms, proxy.ts: 6ms, render: 4ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=ced55414-84b5-4d88-8af6-9a2d54f42b36 200 in 18ms (compile: 1665µs, proxy.ts: 6ms, render: 10ms) + GET /documents/dba2659d-b9fd-4a3b-a28a-448756ea7768 200 in 282ms (compile: 110ms, proxy.ts: 6ms, render: 166ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=dba2659d-b9fd-4a3b-a28a-448756ea7768 200 in 33ms (compile: 2ms, proxy.ts: 8ms, render: 23ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 111ms (compile: 96ms, proxy.ts: 8ms, render: 7ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 111ms (compile: 97ms, proxy.ts: 8ms, render: 5ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=dba2659d-b9fd-4a3b-a28a-448756ea7768 200 in 19ms (compile: 1920µs, proxy.ts: 7ms, render: 11ms) + GET /documents/0afdf760-a9aa-43e1-8b76-400d84b96b2a 200 in 279ms (compile: 111ms, proxy.ts: 6ms, render: 162ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=0afdf760-a9aa-43e1-8b76-400d84b96b2a 200 in 34ms (compile: 2ms, proxy.ts: 10ms, render: 22ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 106ms (compile: 92ms, proxy.ts: 7ms, render: 8ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 107ms (compile: 95ms, proxy.ts: 7ms, render: 5ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=0afdf760-a9aa-43e1-8b76-400d84b96b2a 200 in 19ms (compile: 1664µs, proxy.ts: 6ms, render: 10ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=0afdf760-a9aa-43e1-8b76-400d84b96b2a 200 in 21ms (compile: 1667µs, proxy.ts: 6ms, render: 13ms) + GET /documents/7575b06a-16a9-48d1-94fc-2b7532da75ca?preview=sidebar 200 in 450ms (compile: 106ms, proxy.ts: 7ms, render: 337ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=7575b06a-16a9-48d1-94fc-2b7532da75ca 200 in 18ms (compile: 1688µs, proxy.ts: 6ms, render: 10ms) + GET /api/backend/health 200 in 13ms (compile: 1891µs, proxy.ts: 8ms, render: 4ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 303ms (compile: 1360µs, proxy.ts: 6ms, render: 296ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 106ms (compile: 92ms, proxy.ts: 7ms, render: 6ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 106ms (compile: 94ms, proxy.ts: 7ms, render: 5ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 13ms (compile: 3ms, proxy.ts: 7ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 20ms (compile: 6ms, proxy.ts: 7ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 21ms (compile: 10ms, proxy.ts: 8ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 17ms (compile: 4ms, proxy.ts: 8ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 17ms (compile: 5ms, proxy.ts: 8ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 13ms (compile: 5ms, proxy.ts: 6ms, render: 2ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 346ms (compile: 127ms, proxy.ts: 31ms, render: 188ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 413ms (compile: 292ms, proxy.ts: 18ms, render: 103ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 410ms (compile: 300ms, proxy.ts: 16ms, render: 94ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 412ms (compile: 163ms, proxy.ts: 195ms, render: 54ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 410ms (compile: 164ms, proxy.ts: 193ms, render: 53ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 110ms (compile: 12ms, proxy.ts: 93ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 21ms (compile: 9ms, proxy.ts: 9ms, render: 4ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 20ms (compile: 6ms, proxy.ts: 12ms, render: 3ms) + GET /documents/6700d014-d576-4279-8d76-4787c9156989?preview=sidebar 200 in 662ms (compile: 139ms, proxy.ts: 6ms, render: 517ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=6700d014-d576-4279-8d76-4787c9156989 200 in 128ms (compile: 8ms, proxy.ts: 15ms, render: 105ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 132ms (compile: 102ms, proxy.ts: 15ms, render: 15ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 132ms (compile: 105ms, proxy.ts: 15ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 134ms (compile: 106ms, proxy.ts: 16ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 133ms (compile: 109ms, proxy.ts: 17ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 127ms (compile: 108ms, proxy.ts: 14ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 42ms (compile: 13ms, proxy.ts: 17ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 42ms (compile: 14ms, proxy.ts: 17ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 42ms (compile: 14ms, proxy.ts: 19ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 42ms (compile: 14ms, proxy.ts: 21ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 38ms (compile: 14ms, proxy.ts: 17ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 38ms (compile: 15ms, proxy.ts: 18ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 42ms (compile: 19ms, proxy.ts: 12ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 42ms (compile: 20ms, proxy.ts: 12ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 42ms (compile: 19ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 43ms (compile: 20ms, proxy.ts: 15ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 43ms (compile: 17ms, proxy.ts: 19ms, render: 7ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=7575b06a-16a9-48d1-94fc-2b7532da75ca 200 in 44ms (compile: 4ms, proxy.ts: 20ms, render: 19ms) + GET /api/backend/health 200 in 20ms (compile: 5ms, proxy.ts: 8ms, render: 8ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 320ms (compile: 12ms, proxy.ts: 16ms, render: 292ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 335ms (compile: 6ms, proxy.ts: 8ms, render: 321ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 119ms (compile: 95ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 119ms (compile: 96ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 13ms (compile: 3ms, proxy.ts: 7ms, render: 2ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 183ms (compile: 93ms, proxy.ts: 15ms, render: 75ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 35ms (compile: 11ms, proxy.ts: 12ms, render: 13ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 35ms (compile: 13ms, proxy.ts: 12ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 36ms (compile: 12ms, proxy.ts: 12ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 35ms (compile: 13ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 36ms (compile: 16ms, proxy.ts: 14ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 36ms (compile: 15ms, proxy.ts: 10ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 35ms (compile: 16ms, proxy.ts: 10ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 35ms (compile: 15ms, proxy.ts: 11ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 36ms (compile: 15ms, proxy.ts: 12ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 36ms (compile: 16ms, proxy.ts: 13ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 32ms (compile: 16ms, proxy.ts: 13ms, render: 3ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 41ms (compile: 14ms, proxy.ts: 16ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 40ms (compile: 15ms, proxy.ts: 16ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 41ms (compile: 13ms, proxy.ts: 18ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 36ms (compile: 14ms, proxy.ts: 14ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 36ms (compile: 15ms, proxy.ts: 14ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 36ms (compile: 15ms, proxy.ts: 15ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 41ms (compile: 13ms, proxy.ts: 18ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 41ms (compile: 13ms, proxy.ts: 18ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 41ms (compile: 14ms, proxy.ts: 18ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 41ms (compile: 14ms, proxy.ts: 18ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 41ms (compile: 14ms, proxy.ts: 20ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 41ms (compile: 15ms, proxy.ts: 21ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 130ms (compile: 105ms, proxy.ts: 11ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 128ms (compile: 104ms, proxy.ts: 12ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 129ms (compile: 104ms, proxy.ts: 14ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 128ms (compile: 104ms, proxy.ts: 15ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 130ms (compile: 105ms, proxy.ts: 18ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 129ms (compile: 105ms, proxy.ts: 18ms, render: 6ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=6700d014-d576-4279-8d76-4787c9156989 200 in 21ms (compile: 2ms, proxy.ts: 7ms, render: 11ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 66ms (compile: 5ms, proxy.ts: 7ms, render: 54ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 64ms (compile: 6ms, proxy.ts: 9ms, render: 49ms) + GET /documents/cbebb4af-a158-43c5-a55f-6a2a2d18d491?preview=sidebar 200 in 454ms (compile: 111ms, proxy.ts: 6ms, render: 337ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=cbebb4af-a158-43c5-a55f-6a2a2d18d491 200 in 21ms (compile: 1850µs, proxy.ts: 6ms, render: 14ms) + GET /api/backend/health 200 in 10ms (compile: 1522µs, proxy.ts: 6ms, render: 3ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 109ms (compile: 95ms, proxy.ts: 7ms, render: 8ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 110ms (compile: 98ms, proxy.ts: 8ms, render: 5ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js 200 in 12ms (compile: 3ms, proxy.ts: 7ms, render: 2ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_blockquote.js 200 in 43ms (compile: 13ms, proxy.ts: 19ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bold.js 200 in 43ms (compile: 15ms, proxy.ts: 20ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js 200 in 44ms (compile: 15ms, proxy.ts: 19ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_bullet_list.js 200 in 44ms (compile: 16ms, proxy.ts: 20ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code.js 200 in 44ms (compile: 14ms, proxy.ts: 24ms, render: 5ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_document.js 200 in 43ms (compile: 15ms, proxy.ts: 18ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_code_block.js 200 in 43ms (compile: 15ms, proxy.ts: 18ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_dropcursor.js 200 in 43ms (compile: 16ms, proxy.ts: 18ms, render: 8ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_gapcursor.js 200 in 35ms (compile: 12ms, proxy.ts: 17ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_hard_break.js 200 in 35ms (compile: 13ms, proxy.ts: 17ms, render: 5ms) + GET /api/mnote-web/stream?workspaceId=8f317132-506f-4399-99dc-d22318427248 200 in 688ms (compile: 1358µs, proxy.ts: 5ms, render: 681ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_heading.js 200 in 42ms (compile: 17ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_image.js 200 in 42ms (compile: 18ms, proxy.ts: 13ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_highlight.js 200 in 42ms (compile: 19ms, proxy.ts: 13ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_history.js 200 in 43ms (compile: 18ms, proxy.ts: 16ms, render: 9ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_horizontal_rule.js 200 in 43ms (compile: 18ms, proxy.ts: 18ms, render: 7ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_italic.js 200 in 39ms (compile: 18ms, proxy.ts: 14ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_list_item.js 200 in 42ms (compile: 14ms, proxy.ts: 12ms, render: 16ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_link.js 200 in 42ms (compile: 15ms, proxy.ts: 12ms, render: 15ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_ordered_list.js 200 in 42ms (compile: 16ms, proxy.ts: 12ms, render: 14ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_paragraph.js 200 in 41ms (compile: 16ms, proxy.ts: 13ms, render: 12ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_strike.js 200 in 43ms (compile: 17ms, proxy.ts: 15ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_placeholder.js 200 in 42ms (compile: 19ms, proxy.ts: 17ms, render: 6ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_item.js 200 in 129ms (compile: 98ms, proxy.ts: 12ms, render: 19ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_align.js 200 in 129ms (compile: 100ms, proxy.ts: 12ms, render: 17ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text.js 200 in 129ms (compile: 101ms, proxy.ts: 12ms, render: 16ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_task_list.js 200 in 129ms (compile: 101ms, proxy.ts: 13ms, render: 15ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_text_style.js 200 in 130ms (compile: 103ms, proxy.ts: 15ms, render: 11ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js 200 in 130ms (compile: 104ms, proxy.ts: 16ms, render: 10ms) + GET /api/leptos-tiptap-runtime/snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js 200 in 15ms (compile: 6ms, proxy.ts: 6ms, render: 3ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=cbebb4af-a158-43c5-a55f-6a2a2d18d491 200 in 21ms (compile: 2ms, proxy.ts: 6ms, render: 12ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 54ms (compile: 3ms, proxy.ts: 5ms, render: 47ms) + GET /api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island_bg.wasm 200 in 87ms (compile: 3ms, proxy.ts: 7ms, render: 77ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=0afdf760-a9aa-43e1-8b76-400d84b96b2a 200 in 22ms (compile: 1659µs, proxy.ts: 7ms, render: 14ms) + GET /documents/7575b06a-16a9-48d1-94fc-2b7532da75ca 200 in 270ms (compile: 110ms, proxy.ts: 5ms, render: 155ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=7575b06a-16a9-48d1-94fc-2b7532da75ca 200 in 24ms (compile: 1557µs, proxy.ts: 6ms, render: 16ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 112ms (compile: 95ms, proxy.ts: 6ms, render: 10ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 11ms (compile: 2ms, proxy.ts: 6ms, render: 3ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=7575b06a-16a9-48d1-94fc-2b7532da75ca 200 in 15ms (compile: 1603µs, proxy.ts: 6ms, render: 7ms) + GET /documents/6700d014-d576-4279-8d76-4787c9156989 200 in 285ms (compile: 115ms, proxy.ts: 7ms, render: 163ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=6700d014-d576-4279-8d76-4787c9156989 200 in 38ms (compile: 2ms, proxy.ts: 20ms, render: 15ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 107ms (compile: 93ms, proxy.ts: 7ms, render: 6ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 107ms (compile: 94ms, proxy.ts: 8ms, render: 5ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=6700d014-d576-4279-8d76-4787c9156989 200 in 16ms (compile: 3ms, proxy.ts: 6ms, render: 8ms) + GET /documents/11181c74-1333-41d1-ac84-9b8e027131db 200 in 295ms (compile: 114ms, proxy.ts: 18ms, render: 163ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=filetree&channel=sidebar-file-tree-shell&host=sidebar-file-tree-shell&activeDocumentId=11181c74-1333-41d1-ac84-9b8e027131db 200 in 32ms (compile: 1970µs, proxy.ts: 8ms, render: 22ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 113ms (compile: 99ms, proxy.ts: 7ms, render: 7ms) + GET /api/leptos-tiptap-runtime/manifest.json 200 in 114ms (compile: 101ms, proxy.ts: 9ms, render: 4ms) + GET /api/references/backlinks?workspaceId=8f317132-506f-4399-99dc-d22318427248&pageId=11181c74-1333-41d1-ac84-9b8e027131db 200 in 18ms (compile: 1650µs, proxy.ts: 6ms, render: 11ms) + GET /api/tree/shell?workspaceId=8f317132-506f-4399-99dc-d22318427248&mode=page&channel=sidebar-page-tree-shell&host=sidebar-page-tree-shell&activeDocumentId=11181c74-1333-41d1-ac84-9b8e027131db 200 in 20ms (compile: 1814µs, proxy.ts: 6ms, render: 12ms) diff --git a/wolai-frontend/next-dev-codex.out b/wolai-frontend/next-dev-codex.out new file mode 100644 index 00000000..e69de29b diff --git a/wolai-frontend/src/app/(app)/layout.tsx b/wolai-frontend/src/app/(app)/layout.tsx index a43b0e3e..fd670aef 100644 --- a/wolai-frontend/src/app/(app)/layout.tsx +++ b/wolai-frontend/src/app/(app)/layout.tsx @@ -12,6 +12,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) { sidebarInitialData, } = await loadSidebarDataFromConvex({ client, + auth, fallbackName: auth.name ?? auth.email ?? "我的空间", }); diff --git a/wolai-frontend/src/app/api/documents/create/route.ts b/wolai-frontend/src/app/api/documents/create/route.ts index 1281f1d8..8c6aa1af 100644 --- a/wolai-frontend/src/app/api/documents/create/route.ts +++ b/wolai-frontend/src/app/api/documents/create/route.ts @@ -1,119 +1,83 @@ -import { randomUUID } from "crypto"; import { NextResponse } from "next/server"; import { isConvexEnabled } from "@/lib/convex/enabled"; -import { api } from "@/lib/convex/api"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executePageLifecycleBridgeCommand, - type DocumentCreatePayload, -} from "@/lib/documents/page-command-adapter"; -import { ensureDocumentScaffold } from "@/lib/documents/page-lifecycle-side-effects"; + +type TreeCreateResponse = { + requestId?: string; + traceId?: string; + result?: { + documentId?: string; + parentId?: string | null; + title?: string | null; + sortOrder?: number | null; + workspaceId?: string | null; + execution?: { + access_scope?: "private" | "shared" | "public"; + is_template?: boolean; + created_at?: string | null; + updated_at?: string | null; + } | null; + } | null; +}; export async function POST(request: Request) { - try { - if (isConvexEnabled()) { - return await handleCreateRequestConvex(request); - } - return await handleCreateRequest(); - } catch (error) { - console.error("创建页面失败", error); - const message = error instanceof Error ? error.message : "创建页面失败,请稍后再试"; - return NextResponse.json({ error: message }, { status: 500 }); - } -} - -async function handleCreateRequestConvex(request: Request) { - const { auth, client } = await getAuthedConvexClient(); - const { parentId }: { parentId?: string | null } = await request.json(); - - const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { - fallbackName: auth.email ?? auth.name ?? "我的空间", - workspaceIdIfCreate: randomUUID(), - }); - - let workspaceId: string | null = null; - let accessScope: "private" | "shared" | "public" = "private"; - - if (parentId) { - const parentDoc = await client.query(api.documents.getMeta, { - id: parentId, - }); - - if (!parentDoc) { - return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); - } - - workspaceId = parentDoc.workspace_id; - accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope; - } else { - workspaceId = workspaceBootstrap.activeWorkspaceId || null; - } - - if (!workspaceId) { - return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); + if (!isConvexEnabled()) { + return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); } try { - const id = randomUUID(); - const normalizedWorkspaceId = workspaceId.trim(); - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: normalizedWorkspaceId, - }); - const result = await executePageLifecycleBridgeCommand< - DocumentCreatePayload, - { - id: string; - title?: string | null; - parent_id?: string | null; - sort_order?: number | null; - workspace_id?: string; - access_scope?: "private" | "shared" | "public"; - is_template?: boolean; - } - >({ - context: bridgeContext, - envelope: buildDocumentCommandEnvelope({ - name: "documents.create", - payload: { - documentId: id, - workspaceId: normalizedWorkspaceId, - parentId: parentId?.trim() || null, - title: "无标题", - accessScope, - content: [], - }, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: id, - }, + const { parentId }: { parentId?: string | null } = await request.json(); + const upstreamUrl = new URL("/api/tree/commands", request.url); + const response = await fetch(upstreamUrl.toString(), { + method: "POST", + headers: new Headers({ + "content-type": "application/json", + }), + body: JSON.stringify({ + action: "create", + parentId: typeof parentId === "string" ? parentId.trim() || null : null, }), }); - await ensureDocumentScaffold(result.result.id, result.result.title ?? "无标题"); + const payload = (await response.json().catch(() => null)) as TreeCreateResponse | { error?: string } | null; + if (!response.ok) { + return NextResponse.json( + { + error: + payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string" + ? payload.error + : "创建页面失败,请稍后再试", + }, + { status: response.status }, + ); + } return NextResponse.json({ - ...result.result, + id: payload?.result?.documentId ?? "", + title: payload?.result?.title ?? "无标题", + parent_id: payload?.result?.parentId ?? null, + sort_order: payload?.result?.sortOrder ?? null, + workspace_id: payload?.result?.workspaceId ?? undefined, + access_scope: payload?.result?.execution?.access_scope ?? "private", + is_template: payload?.result?.execution?.is_template ?? false, + created_at: payload?.result?.execution?.created_at ?? null, + updated_at: + payload?.result?.execution?.updated_at ?? + payload?.result?.execution?.created_at ?? + null, meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, + requestId: payload?.requestId, + traceId: payload?.traceId, + commandName: "tree.node.create", }, }); } catch (error) { - return documentBridgeErrorResponse(error); + return NextResponse.json( + { + error: error instanceof Error ? error.message : "创建页面失败,请稍后再试", + }, + { status: 500 }, + ); } } -async function handleCreateRequest() { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/documents/move/route.ts b/wolai-frontend/src/app/api/documents/move/route.ts index 9bf2304f..06527d49 100644 --- a/wolai-frontend/src/app/api/documents/move/route.ts +++ b/wolai-frontend/src/app/api/documents/move/route.ts @@ -1,67 +1,79 @@ import { NextResponse } from "next/server"; import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - assertDocumentId, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executePageLifecycleBridgeCommand, - type DocumentMovePayload, -} from "@/lib/documents/page-command-adapter"; -interface MovePayload { - documentId: string; +type TreeMoveResponse = { + requestId?: string; + traceId?: string; + result?: { + documentId?: string; + parentId?: string | null; + sortOrder?: number | null; + } | null; +}; + +type MovePayload = { + documentId?: string | null; parentId?: string | null; - position: number; + position?: number | null; workspaceId?: string | null; -} +}; export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { documentId, parentId = null, position, workspaceId }: MovePayload = await request.json(); - const normalizedDocumentId = assertDocumentId(documentId); - const normalizedWorkspaceId = workspaceId?.trim() || null; - const sortOrder = Number.isFinite(position) ? Math.floor(position) : 0; - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: normalizedWorkspaceId, - }); - const result = await executePageLifecycleBridgeCommand({ - context: bridgeContext, - envelope: buildDocumentCommandEnvelope({ - name: "documents.move", - payload: { - documentId: normalizedDocumentId, - workspaceId: normalizedWorkspaceId, - parentId: parentId?.trim() || null, - sortOrder, - }, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: normalizedDocumentId, - }, - }), - }); - - return NextResponse.json({ - ok: true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } + if (!isConvexEnabled()) { + return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); } - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); + try { + const { documentId, parentId = null, position, workspaceId }: MovePayload = await request.json(); + const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : ""; + if (!normalizedDocumentId) { + return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); + } + + const upstreamUrl = new URL("/api/tree/commands", request.url); + const response = await fetch(upstreamUrl.toString(), { + method: "POST", + headers: new Headers({ + "content-type": "application/json", + }), + body: JSON.stringify({ + action: "move", + documentId: normalizedDocumentId, + parentId: typeof parentId === "string" ? parentId.trim() || null : null, + sortOrder: typeof position === "number" && Number.isFinite(position) ? Math.floor(position) : 0, + workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null, + }), + }); + + const payload = (await response.json().catch(() => null)) as TreeMoveResponse | { error?: string } | null; + if (!response.ok) { + return NextResponse.json( + { + error: + payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string" + ? payload.error + : "移动失败,请稍后再试", + }, + { status: response.status }, + ); + } + + return NextResponse.json({ + ok: true, + meta: { + requestId: payload?.requestId, + traceId: payload?.traceId, + commandName: "tree.subtree.move", + }, + }); + } catch (error) { + return NextResponse.json( + { + error: error instanceof Error ? error.message : "移动失败,请稍后再试", + }, + { status: 500 }, + ); + } } export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/documents/route-adapters.test.ts b/wolai-frontend/src/app/api/documents/route-adapters.test.ts index a311e728..f28ed48f 100644 --- a/wolai-frontend/src/app/api/documents/route-adapters.test.ts +++ b/wolai-frontend/src/app/api/documents/route-adapters.test.ts @@ -79,6 +79,8 @@ import { POST as postPurge } from "@/app/api/documents/purge/route"; import { POST as postTitle } from "@/app/api/documents/title/route"; import { POST as postOptions } from "@/app/api/documents/options/route"; import { POST as postSave } from "@/app/api/documents/save/route"; +import { POST as postCreate } from "@/app/api/documents/create/route"; +import { POST as postMove } from "@/app/api/documents/move/route"; import { GET as getPage } from "@/app/api/documents/page/route"; import { executeDocumentCreateChildBridgeCommand, @@ -91,6 +93,151 @@ import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-comman import { loadPageAggregate } from "@/lib/documents/page-aggregate-loader"; describe("documents route adapters", () => { + it("create route 作为 compat 壳委托 tree commands 主路径", async () => { + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + requestId: "req_tree_create_1", + traceId: "trace_tree_create_1", + result: { + action: "create", + workspaceId: "ws_1", + documentId: "doc_new", + parentId: null, + title: "无标题", + sortOrder: 0, + updatedAt: "2026-04-23T00:00:00Z", + execution: { + access_scope: "private", + is_template: false, + created_at: "2026-04-23T00:00:00Z", + updated_at: "2026-04-23T00:00:00Z", + }, + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + + const response = await postCreate(new Request("http://localhost/api/documents/create", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ parentId: null }), + })); + const payload = await response.json() as { + id: string; + workspace_id: string; + meta: { commandName: string }; + }; + + expect(fetchMock).toHaveBeenCalledWith( + "http://localhost/api/tree/commands", + expect.objectContaining({ + method: "POST", + headers: expect.any(Headers), + body: JSON.stringify({ + action: "create", + parentId: null, + }), + }), + ); + expect(payload.id).toBe("doc_new"); + expect(payload.workspace_id).toBe("ws_1"); + expect(payload.meta.commandName).toBe("tree.node.create"); + }); + + it("move route 作为 compat 壳委托 tree commands 主路径", async () => { + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + requestId: "req_tree_move_1", + traceId: "trace_tree_move_1", + result: { + action: "move", + workspaceId: "ws_1", + documentId: "doc_1", + parentId: "parent_1", + sortOrder: 2, + updatedAt: "2026-04-23T00:00:00Z", + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + + const response = await postMove(new Request("http://localhost/api/documents/move", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ documentId: "doc_1", parentId: "parent_1", position: 2.7, workspaceId: "ws_1" }), + })); + const payload = await response.json() as { + ok: boolean; + meta: { commandName: string }; + }; + + expect(fetchMock).toHaveBeenCalledWith( + "http://localhost/api/tree/commands", + expect.objectContaining({ + method: "POST", + headers: expect.any(Headers), + body: JSON.stringify({ + action: "move", + documentId: "doc_1", + parentId: "parent_1", + sortOrder: 2, + workspaceId: "ws_1", + }), + }), + ); + expect(payload.ok).toBe(true); + expect(payload.meta.commandName).toBe("tree.subtree.move"); + }); + + it("title route 在树重命名兼容请求下委托 tree commands 主路径", async () => { + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + requestId: "req_tree_rename_1", + traceId: "trace_tree_rename_1", + result: { + action: "rename", + workspaceId: "ws_1", + documentId: "doc_1", + title: "新标题", + updatedAt: "2026-04-23T00:00:00Z", + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + + const response = await postTitle(new Request("http://localhost/api/documents/title", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }), + })); + const payload = await response.json() as { + ok: boolean; + meta: { commandName: string }; + }; + + expect(fetchMock).toHaveBeenCalledWith( + "http://localhost/api/tree/commands", + expect.objectContaining({ + method: "POST", + headers: expect.any(Headers), + body: JSON.stringify({ + action: "rename", + documentId: "doc_1", + workspaceId: "ws_1", + title: "新标题", + }), + }), + ); + expect(payload.ok).toBe(true); + expect(payload.meta.commandName).toBe("tree.node.rename"); + }); + it("creates child route delegates to unified adapter", async () => { await postCreateChild(new Request("http://localhost/api/documents/create-child", { method: "POST", @@ -146,10 +293,15 @@ describe("documents route adapters", () => { expect(payload.meta.queryName).toBe("documents.page.get"); }); - it("title route delegates to unified page write adapter", async () => { + it("title route 在 page head 请求下仍委托 unified page write adapter", async () => { await postTitle(new Request("http://localhost/api/documents/title", { method: "POST", - body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }), + body: JSON.stringify({ + documentId: "doc_1", + workspaceId: "ws_1", + title: "新标题", + commandName: "page.head.updateTitle", + }), })); expect(executePageWriteBridgeCommand).toHaveBeenCalled(); diff --git a/wolai-frontend/src/app/api/documents/title/route.ts b/wolai-frontend/src/app/api/documents/title/route.ts index 89365423..46e6fb78 100644 --- a/wolai-frontend/src/app/api/documents/title/route.ts +++ b/wolai-frontend/src/app/api/documents/title/route.ts @@ -17,15 +17,60 @@ interface RenamePayload { documentId: string; workspaceId?: string | null; title: string; + commandName?: string | null; } +type TreeRenameResponse = { + requestId?: string; + traceId?: string; +}; + export async function POST(request: Request) { if (isConvexEnabled()) { try { - const { documentId, workspaceId, title }: RenamePayload = await request.json(); + const { documentId, workspaceId, title, commandName }: RenamePayload = await request.json(); const normalizedDocumentId = assertDocumentId(documentId); const normalizedTitle = assertTitle(title); const normalizedWorkspaceId = workspaceId?.trim() || null; + + if (commandName !== PAGE_COMMAND_NAMES.updateTitle) { + const upstreamUrl = new URL("/api/tree/commands", request.url); + const response = await fetch(upstreamUrl.toString(), { + method: "POST", + headers: new Headers({ + "content-type": "application/json", + }), + body: JSON.stringify({ + action: "rename", + documentId: normalizedDocumentId, + workspaceId: normalizedWorkspaceId, + title: normalizedTitle, + }), + }); + + const payload = (await response.json().catch(() => null)) as TreeRenameResponse | { error?: string } | null; + if (!response.ok) { + return NextResponse.json( + { + error: + payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string" + ? payload.error + : "重命名失败,请稍后再试", + }, + { status: response.status }, + ); + } + + return NextResponse.json({ + ok: true, + meta: { + requestId: payload?.requestId, + traceId: payload?.traceId, + commandName: "tree.node.rename", + }, + }); + } + const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId }); const envelope = buildDocumentCommandEnvelope({ name: PAGE_COMMAND_NAMES.updateTitle, diff --git a/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts b/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts index 44b80c95..d0729b47 100644 --- a/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts +++ b/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts @@ -5,6 +5,11 @@ const mockBuildDocumentBridgeContext = vi.fn(); const mockBuildDocumentQueryEnvelope = vi.fn(); const mockExecuteRustBridgeQueryTransport = vi.fn(); const mockResolveRustBridgeQueryPlan = vi.fn(); +const mockResolveKernelFileTreeProjection = vi.fn(); +const mockAttachKernelFileTreeProjection = vi.fn((input: { dataset: unknown; projection: unknown }) => ({ + ...(input.dataset as Record), + kernel_file_tree_projection: input.projection, +})); const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => Response.json( { @@ -29,6 +34,11 @@ vi.mock("@/lib/documents/rust-runtime", () => ({ resolveRustBridgeQueryPlan: mockResolveRustBridgeQueryPlan, })); +vi.mock("@/lib/server/kernel-file-tree", () => ({ + resolveKernelFileTreeProjection: (...args: unknown[]) => mockResolveKernelFileTreeProjection(...args), + attachKernelFileTreeProjection: (...args: unknown[]) => mockAttachKernelFileTreeProjection(...args), +})); + describe("/api/mnote-web/stream route", () => { beforeEach(() => { mockGetAuthedConvexClient.mockReset(); @@ -36,11 +46,18 @@ describe("/api/mnote-web/stream route", () => { mockBuildDocumentQueryEnvelope.mockReset(); mockExecuteRustBridgeQueryTransport.mockReset(); mockResolveRustBridgeQueryPlan.mockReset(); + mockResolveKernelFileTreeProjection.mockReset(); + mockAttachKernelFileTreeProjection.mockClear(); mockDocumentBridgeErrorResponse.mockClear(); }); it("直接在 3000 内生成 snapshot SSE,不再回源 mnote-web", async () => { mockGetAuthedConvexClient.mockResolvedValue({ + auth: { + userId: "user_1", + email: "dev@example.com", + name: "开发用户", + }, client: { query: vi.fn() }, }); mockBuildDocumentBridgeContext.mockResolvedValue({ @@ -107,6 +124,13 @@ describe("/api/mnote-web/stream route", () => { has_more: false, generated_at: "2026-04-22T00:00:00Z", }); + mockResolveKernelFileTreeProjection.mockResolvedValue({ + projectionId: "kernel_projection:file_tree:root", + projection: "file_tree", + rootNodeId: null, + items: [], + edges: [], + }); const { GET } = await import("./route"); const response = await GET( @@ -124,7 +148,103 @@ describe("/api/mnote-web/stream route", () => { expect(text).toContain('"projection":"sidebar_tree"'); expect(text).toContain('"workspaceId":"ws_1"'); expect(text).toContain('"activeWorkspaceId":"ws_1"'); + expect(text).toContain('"kernelFileTreeProjection"'); expect(mockResolveRustBridgeQueryPlan).toHaveBeenCalledTimes(2); expect(mockExecuteRustBridgeQueryTransport).toHaveBeenCalledTimes(2); + expect(mockResolveKernelFileTreeProjection).toHaveBeenCalledTimes(1); + }); + + it("应把请求 cursor 继续透传到 overview query 和 snapshot envelope", async () => { + mockGetAuthedConvexClient.mockResolvedValue({ + auth: { + userId: "user_1", + email: "dev@example.com", + name: "开发用户", + }, + client: { query: vi.fn() }, + }); + mockBuildDocumentBridgeContext.mockResolvedValue({ + requestId: "req_stream_2", + traceId: "trace_stream_2", + workspaceId: "ws_1", + }); + mockBuildDocumentQueryEnvelope + .mockReturnValueOnce({ + name: "sidebar.dataset.list", + payload: { workspaceId: "ws_1" }, + }) + .mockReturnValueOnce({ + name: "bridge.workspace.overview", + payload: { + workspaceId: "ws_1", + limit: 20, + cursor: "evt_9", + commandStatus: null, + eventStatus: null, + targetPageId: null, + targetBlockId: null, + aggregateType: null, + aggregateId: null, + }, + }); + mockResolveRustBridgeQueryPlan + .mockResolvedValueOnce({ kind: "query", functionName: "sidebar:datasetList", argsJson: { workspaceId: "ws_1" } }) + .mockResolvedValueOnce({ + kind: "query", + functionName: "bridgeLogs:listWorkspaceOverview", + argsJson: { workspaceId: "ws_1", cursor: "evt_9" }, + }); + mockExecuteRustBridgeQueryTransport + .mockResolvedValueOnce({ + active_workspace_id: "ws_1", + workspaces: [], + documents: [], + trashed_documents: [], + media_assets: [], + trashed_media_assets: [], + mindmap_assets: [], + trashed_mindmap_assets: [], + table_assets: [], + trashed_table_assets: [], + mindmap_docs: [], + mindmap_asset_children: {}, + }) + .mockResolvedValueOnce({ + workspace_id: "ws_1", + command_logs: [], + domain_events: [], + counts: { command_logs: 0, domain_events: 0 }, + filters: null, + next_cursor: "evt_10", + has_more: true, + generated_at: "2026-04-22T00:00:00Z", + }); + mockResolveKernelFileTreeProjection.mockResolvedValue({ + projectionId: "kernel_projection:file_tree:root", + projection: "file_tree", + rootNodeId: null, + items: [], + edges: [], + }); + + const { GET } = await import("./route"); + const response = await GET( + new Request("http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1&cursor=evt_9", { + method: "GET", + }), + ); + + expect(response.status).toBe(200); + const text = await response.text(); + expect(text).toContain('"cursor":"evt_9"'); + expect(mockBuildDocumentQueryEnvelope).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + payload: expect.objectContaining({ + workspaceId: "ws_1", + cursor: "evt_9", + }), + }), + ); }); }); diff --git a/wolai-frontend/src/app/api/mnote-web/stream/route.ts b/wolai-frontend/src/app/api/mnote-web/stream/route.ts index 9f3c92e0..35c66af4 100644 --- a/wolai-frontend/src/app/api/mnote-web/stream/route.ts +++ b/wolai-frontend/src/app/api/mnote-web/stream/route.ts @@ -9,6 +9,10 @@ import { resolveRustBridgeQueryPlan, } from "@/lib/documents/rust-runtime"; import { mapSidebarDatasetListQueryResultToInitialData } from "@/lib/sidebar-data"; +import { + attachKernelFileTreeProjection, + resolveKernelFileTreeProjection, +} from "@/lib/server/kernel-file-tree"; export const dynamic = "force-dynamic"; @@ -26,7 +30,7 @@ export async function GET(request: Request) { return Response.json({ error: "缺少 workspaceId" }, { status: 400 }); } - const { client } = await getAuthedConvexClient(); + const { auth, client } = await getAuthedConvexClient(); const context = await buildDocumentBridgeContext({ request, workspaceId, @@ -46,6 +50,20 @@ export async function GET(request: Request) { client, plan: sidebarPlan, }); + const sidebarDatasetWithFileTree = attachKernelFileTreeProjection({ + dataset: sidebarDataset, + projection: await resolveKernelFileTreeProjection({ + client, + request, + workspaceId, + actor: { + actorType: "user", + actorId: auth.userId, + sessionId: null, + }, + dataset: sidebarDataset, + }), + }); const overviewEnvelope = buildDocumentQueryEnvelope({ name: "bridge.workspace.overview", @@ -79,10 +97,13 @@ export async function GET(request: Request) { cursor, requestId: context.requestId, traceId: context.traceId, - data: mapSidebarDatasetListQueryResultToInitialData(sidebarDataset), + data: mapSidebarDatasetListQueryResultToInitialData(sidebarDatasetWithFileTree), snapshot: { - dataset: sidebarDataset, - tree: sidebarDataset.kernel_sidebar_projection ?? sidebarDataset.kernelSidebarProjection ?? null, + dataset: sidebarDatasetWithFileTree, + tree: + sidebarDatasetWithFileTree.kernel_sidebar_projection ?? + sidebarDatasetWithFileTree.kernelSidebarProjection ?? + null, }, overview, }; diff --git a/wolai-frontend/src/app/api/sidebar/route.ts b/wolai-frontend/src/app/api/sidebar/route.ts index 17f82256..b22ead77 100644 --- a/wolai-frontend/src/app/api/sidebar/route.ts +++ b/wolai-frontend/src/app/api/sidebar/route.ts @@ -16,6 +16,10 @@ import { mapSidebarDatasetListQueryResultToInitialData, type SidebarDatasetListQueryResult, } from "@/lib/sidebar-data"; +import { + attachKernelFileTreeProjection, + resolveKernelFileTreeProjection, +} from "@/lib/server/kernel-file-tree"; export const dynamic = "force-dynamic"; @@ -54,7 +58,21 @@ export async function GET(request: Request) { client, plan, }); - const result = mapSidebarDatasetListQueryResultToInitialData(sidebarDataset); + const sidebarDatasetWithFileTree = attachKernelFileTreeProjection({ + dataset: sidebarDataset, + projection: await resolveKernelFileTreeProjection({ + client, + request, + workspaceId: targetWorkspaceId, + actor: { + actorType: "user", + actorId: auth.userId, + sessionId: null, + }, + dataset: sidebarDataset, + }), + }); + const result = mapSidebarDatasetListQueryResultToInitialData(sidebarDatasetWithFileTree); return NextResponse.json({ ...result, diff --git a/wolai-frontend/src/app/api/tree/commands/route.test.ts b/wolai-frontend/src/app/api/tree/commands/route.test.ts new file mode 100644 index 00000000..bf6033a5 --- /dev/null +++ b/wolai-frontend/src/app/api/tree/commands/route.test.ts @@ -0,0 +1,374 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockIsConvexEnabled = vi.fn(() => true); +const mockGetAuthedConvexClient = vi.fn(); +const mockBuildDocumentBridgeContext = vi.fn(); +const mockBuildDocumentCommandEnvelope = vi.fn(); +const mockResolveRustBridgeCommandPlan = vi.fn(); +const mockExecuteRustBridgeMutationTransport = vi.fn(); +const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => + Response.json( + { + error: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ), +); +const mockEnsureDocumentScaffold = vi.fn(); + +vi.mock("@/lib/convex/enabled", () => ({ + isConvexEnabled: () => mockIsConvexEnabled(), +})); + +vi.mock("@/lib/convex/api", () => ({ + api: { + documents: { + getMeta: "documents:getMeta", + }, + workspaces: { + ensureDefaultWorkspace: "workspaces:ensureDefaultWorkspace", + }, + }, +})); + +vi.mock("@/lib/convex/route", () => ({ + getAuthedConvexClient: mockGetAuthedConvexClient, +})); + +vi.mock("@/lib/documents/bridge", () => ({ + assertDocumentId: (value: string | null | undefined) => { + const normalized = typeof value === "string" ? value.trim() : ""; + if (!normalized) { + throw new Error("缺少 documentId"); + } + return normalized; + }, + assertTitle: (value: string | null | undefined) => { + const normalized = typeof value === "string" ? value.trim() : ""; + if (!normalized) { + throw new Error("缺少标题"); + } + return normalized; + }, + buildDocumentBridgeContext: mockBuildDocumentBridgeContext, + buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, + documentBridgeErrorResponse: (...args: unknown[]) => mockDocumentBridgeErrorResponse(...args), +})); + +vi.mock("@/lib/documents/rust-runtime", () => ({ + resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args), + executeRustBridgeMutationTransport: (...args: unknown[]) => mockExecuteRustBridgeMutationTransport(...args), +})); + +vi.mock("@/lib/documents/page-lifecycle-side-effects", () => ({ + ensureDocumentScaffold: (...args: unknown[]) => mockEnsureDocumentScaffold(...args), +})); + +describe("/api/tree/commands route", () => { + beforeEach(() => { + mockIsConvexEnabled.mockReturnValue(true); + mockGetAuthedConvexClient.mockReset(); + mockBuildDocumentBridgeContext.mockReset(); + mockBuildDocumentCommandEnvelope.mockReset(); + mockResolveRustBridgeCommandPlan.mockReset(); + mockExecuteRustBridgeMutationTransport.mockReset(); + mockEnsureDocumentScaffold.mockReset(); + mockDocumentBridgeErrorResponse.mockClear(); + }); + + it("create action 走 tree.node.create,并保留本地 scaffold 副作用", async () => { + const client = { + mutation: vi.fn(async () => ({ activeWorkspaceId: "ws_root" })), + query: vi.fn(), + }; + mockGetAuthedConvexClient.mockResolvedValue({ + auth: { + userId: "user_1", + email: "dev@example.com", + name: "开发用户", + }, + client, + }); + mockBuildDocumentBridgeContext.mockResolvedValue({ + requestId: "req_tree_1", + traceId: "trace_tree_1", + workspaceId: "ws_root", + actor: { + actorType: "user", + actorId: "user_1", + sessionId: null, + }, + source: { + channel: "next-route", + client: "wolai-frontend", + }, + tenantId: null, + deploymentId: null, + projectId: null, + authToken: null, + idempotencyKey: null, + validateOnly: false, + dryRun: false, + }); + mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); + mockResolveRustBridgeCommandPlan.mockResolvedValue({ + kind: "command", + commandName: "tree.node.create", + commandId: "cmd_tree_create_1", + functionName: "documents:createWithParentReference", + workspaceId: "ws_root", + requestId: "req_tree_1", + traceId: "trace_tree_1", + actorId: "user_1", + idempotencyKey: null, + payloadJson: "{}", + argsJson: {}, + }); + mockExecuteRustBridgeMutationTransport.mockResolvedValue({ + id: "doc_new", + title: "无标题", + parent_id: null, + sort_order: 0, + workspace_id: "ws_root", + access_scope: "private", + is_template: false, + created_at: "2026-04-23T00:00:00Z", + updated_at: "2026-04-23T00:00:00Z", + }); + + const { POST } = await import("./route"); + const response = await POST( + new Request("http://127.0.0.1:3000/api/tree/commands", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + action: "create", + parentId: null, + }), + }), + ); + + expect(response.status).toBe(200); + const payload = await response.json() as { + result: { + action: string; + documentId: string; + workspaceId: string; + title: string; + }; + }; + expect(payload.result.action).toBe("create"); + expect(payload.result.documentId).toBe("doc_new"); + expect(payload.result.workspaceId).toBe("ws_root"); + expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( + expect.objectContaining({ + name: "tree.node.create", + payload: expect.objectContaining({ + workspaceId: "ws_root", + parentId: null, + title: "无标题", + accessScope: "private", + }), + }), + ); + expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_new", "无标题"); + }); + + it("move action 走 tree.subtree.move,并把 position 归一化为 sortOrder", async () => { + const client = { + mutation: vi.fn(), + query: vi.fn(async () => ({ + id: "doc_1", + workspace_id: "ws_1", + })), + }; + mockGetAuthedConvexClient.mockResolvedValue({ + auth: { + userId: "user_1", + email: "dev@example.com", + name: "开发用户", + }, + client, + }); + mockBuildDocumentBridgeContext.mockResolvedValue({ + requestId: "req_tree_2", + traceId: "trace_tree_2", + workspaceId: "ws_1", + actor: { + actorType: "user", + actorId: "user_1", + sessionId: null, + }, + source: { + channel: "next-route", + client: "wolai-frontend", + }, + tenantId: null, + deploymentId: null, + projectId: null, + authToken: null, + idempotencyKey: null, + validateOnly: false, + dryRun: false, + }); + mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); + mockResolveRustBridgeCommandPlan.mockResolvedValue({ + kind: "command", + commandName: "tree.subtree.move", + commandId: "cmd_tree_move_1", + functionName: "documents:move", + workspaceId: "ws_1", + requestId: "req_tree_2", + traceId: "trace_tree_2", + actorId: "user_1", + idempotencyKey: null, + payloadJson: "{}", + argsJson: {}, + }); + mockExecuteRustBridgeMutationTransport.mockResolvedValue({ + ok: true, + updated_at: "2026-04-23T00:00:00Z", + }); + + const { POST } = await import("./route"); + const response = await POST( + new Request("http://127.0.0.1:3000/api/tree/commands", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + action: "move", + documentId: "doc_1", + parentId: "parent_1", + sortOrder: 2.9, + workspaceId: "ws_1", + }), + }), + ); + + expect(response.status).toBe(200); + const payload = await response.json() as { + result: { + action: string; + documentId: string; + parentId: string | null; + sortOrder: number | null; + }; + }; + expect(payload.result).toMatchObject({ + action: "move", + documentId: "doc_1", + parentId: "parent_1", + sortOrder: 2, + workspaceId: "ws_1", + }); + expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( + expect.objectContaining({ + name: "tree.subtree.move", + payload: { + documentId: "doc_1", + parentId: "parent_1", + sortOrder: 2, + }, + }), + ); + expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); + }); + + it("rename action 走 tree.node.rename,并保留标题 payload", async () => { + const client = { + mutation: vi.fn(), + query: vi.fn(async () => ({ + id: "doc_1", + workspace_id: "ws_1", + })), + }; + mockGetAuthedConvexClient.mockResolvedValue({ + auth: { + userId: "user_1", + email: "dev@example.com", + name: "开发用户", + }, + client, + }); + mockBuildDocumentBridgeContext.mockResolvedValue({ + requestId: "req_tree_3", + traceId: "trace_tree_3", + workspaceId: "ws_1", + actor: { + actorType: "user", + actorId: "user_1", + sessionId: null, + }, + source: { + channel: "next-route", + client: "wolai-frontend", + }, + tenantId: null, + deploymentId: null, + projectId: null, + authToken: null, + idempotencyKey: null, + validateOnly: false, + dryRun: false, + }); + mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); + mockResolveRustBridgeCommandPlan.mockResolvedValue({ + kind: "command", + commandName: "tree.node.rename", + commandId: "cmd_tree_rename_1", + functionName: "documents:updateTitle", + workspaceId: "ws_1", + requestId: "req_tree_3", + traceId: "trace_tree_3", + actorId: "user_1", + idempotencyKey: null, + payloadJson: "{}", + argsJson: {}, + }); + mockExecuteRustBridgeMutationTransport.mockResolvedValue({ + ok: true, + updated_at: "2026-04-23T00:00:00Z", + }); + + const { POST } = await import("./route"); + const response = await POST( + new Request("http://127.0.0.1:3000/api/tree/commands", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + action: "rename", + documentId: "doc_1", + workspaceId: "ws_1", + title: "新标题", + }), + }), + ); + + expect(response.status).toBe(200); + const payload = await response.json() as { + result: { + action: string; + documentId: string; + workspaceId: string; + title: string; + }; + }; + expect(payload.result).toMatchObject({ + action: "rename", + documentId: "doc_1", + workspaceId: "ws_1", + title: "新标题", + }); + expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( + expect.objectContaining({ + name: "tree.node.rename", + payload: { + documentId: "doc_1", + workspaceId: "ws_1", + title: "新标题", + }, + }), + ); + expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); + }); +}); diff --git a/wolai-frontend/src/app/api/tree/commands/route.ts b/wolai-frontend/src/app/api/tree/commands/route.ts new file mode 100644 index 00000000..662deada --- /dev/null +++ b/wolai-frontend/src/app/api/tree/commands/route.ts @@ -0,0 +1,259 @@ +import { randomUUID } from "node:crypto"; +import { NextResponse } from "next/server"; +import { api } from "@/lib/convex/api"; +import { isConvexEnabled } from "@/lib/convex/enabled"; +import { getAuthedConvexClient } from "@/lib/convex/route"; +import { + assertDocumentId, + assertTitle, + buildDocumentBridgeContext, + buildDocumentCommandEnvelope, + documentBridgeErrorResponse, +} from "@/lib/documents/bridge"; +import { ensureDocumentScaffold } from "@/lib/documents/page-lifecycle-side-effects"; +import { + executeRustBridgeMutationTransport, + resolveRustBridgeCommandPlan, +} from "@/lib/documents/rust-runtime"; + +type TreeCommandPayload = { + action?: "create" | "move" | "rename"; + workspaceId?: string | null; + documentId?: string | null; + parentId?: string | null; + title?: string | null; + accessScope?: "private" | "shared" | "public" | null; + content?: unknown; + sortOrder?: number | null; +}; + +function trimOrNull(value: unknown) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function normalizeTitle(value: unknown) { + const title = typeof value === "string" ? value.trim() : ""; + return title || "无标题"; +} + +function normalizeSortOrder(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; +} + +export async function POST(request: Request) { + if (!isConvexEnabled()) { + return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); + } + + try { + const payload = (await request.json()) as TreeCommandPayload; + switch (payload.action) { + case "create": + return handleCreate(request, payload); + case "move": + return handleMove(request, payload); + case "rename": + return handleRename(request, payload); + default: + return NextResponse.json({ error: "不支持的 tree action" }, { status: 400 }); + } + } catch (error) { + return documentBridgeErrorResponse(error); + } +} + +async function handleCreate(request: Request, payload: TreeCommandPayload) { + const { auth, client } = await getAuthedConvexClient(); + const parentId = trimOrNull(payload.parentId); + const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { + fallbackName: auth.email ?? auth.name ?? "我的空间", + workspaceIdIfCreate: randomUUID(), + }); + + let workspaceId: string | null = trimOrNull(payload.workspaceId); + let accessScope: "private" | "shared" | "public" = "private"; + if (parentId) { + const parentDoc = await client.query(api.documents.getMeta, { id: parentId }); + if (!parentDoc) { + return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); + } + workspaceId = trimOrNull(parentDoc.workspace_id); + accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope; + } else if (!workspaceId) { + workspaceId = trimOrNull(workspaceBootstrap.activeWorkspaceId); + } + + if (!workspaceId) { + return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); + } + + const documentId = trimOrNull(payload.documentId) ?? randomUUID(); + const title = normalizeTitle(payload.title); + const context = await buildDocumentBridgeContext({ + request, + workspaceId, + }); + const envelope = buildDocumentCommandEnvelope({ + name: "tree.node.create", + payload: { + documentId, + workspaceId, + parentId, + title, + accessScope: trimOrNull(payload.accessScope) ?? accessScope, + content: Array.isArray(payload.content) ? payload.content : [], + }, + context, + target: { + workspaceId, + pageId: documentId, + }, + reason: "tree-route create", + refs: ["next-tree-route"], + }); + const plan = await resolveRustBridgeCommandPlan({ + context, + envelope, + }); + const result = await executeRustBridgeMutationTransport<{ + id: string; + title: string | null; + parent_id: string | null; + sort_order: number | null; + workspace_id: string; + access_scope: "private" | "shared" | "public"; + is_template: boolean; + created_at: string; + updated_at: string; + }>({ + client, + plan, + }); + + await ensureDocumentScaffold(result.id, result.title ?? title); + + return NextResponse.json({ + requestId: context.requestId, + traceId: context.traceId, + result: { + action: "create", + workspaceId, + documentId: result.id, + parentId: result.parent_id ?? parentId, + title: result.title ?? title, + sortOrder: result.sort_order ?? null, + updatedAt: result.updated_at ?? null, + execution: result, + }, + }); +} + +async function handleMove(request: Request, payload: TreeCommandPayload) { + const { client } = await getAuthedConvexClient(); + const documentId = assertDocumentId(payload.documentId ?? null); + const sourceDoc = await client.query(api.documents.getMeta, { id: documentId }); + if (!sourceDoc) { + return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); + } + + const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); + const parentId = trimOrNull(payload.parentId); + const sortOrder = normalizeSortOrder(payload.sortOrder); + const context = await buildDocumentBridgeContext({ + request, + workspaceId, + }); + const envelope = buildDocumentCommandEnvelope({ + name: "tree.subtree.move", + payload: { + documentId, + parentId, + sortOrder, + }, + context, + target: { + workspaceId, + pageId: documentId, + }, + reason: "tree-route move", + refs: ["next-tree-route"], + }); + const plan = await resolveRustBridgeCommandPlan({ + context, + envelope, + }); + const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({ + client, + plan, + }); + + return NextResponse.json({ + requestId: context.requestId, + traceId: context.traceId, + result: { + action: "move", + workspaceId, + documentId, + parentId, + sortOrder, + updatedAt: result?.updated_at ?? null, + execution: result ?? null, + }, + }); +} + +async function handleRename(request: Request, payload: TreeCommandPayload) { + const { client } = await getAuthedConvexClient(); + const documentId = assertDocumentId(payload.documentId ?? null); + const sourceDoc = await client.query(api.documents.getMeta, { id: documentId }); + if (!sourceDoc) { + return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); + } + + const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); + const title = assertTitle(payload.title ?? null); + const context = await buildDocumentBridgeContext({ + request, + workspaceId, + }); + const envelope = buildDocumentCommandEnvelope({ + name: "tree.node.rename", + payload: { + documentId, + workspaceId, + title, + }, + context, + target: { + workspaceId, + pageId: documentId, + }, + reason: "tree-route rename", + refs: ["next-tree-route"], + }); + const plan = await resolveRustBridgeCommandPlan({ + context, + envelope, + }); + const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({ + client, + plan, + }); + + return NextResponse.json({ + requestId: context.requestId, + traceId: context.traceId, + result: { + action: "rename", + workspaceId, + documentId, + title, + updatedAt: result?.updated_at ?? null, + execution: result ?? null, + }, + }); +} + +export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/tree/shell/route.test.ts b/wolai-frontend/src/app/api/tree/shell/route.test.ts new file mode 100644 index 00000000..959ef25f --- /dev/null +++ b/wolai-frontend/src/app/api/tree/shell/route.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockResolveMnoteWebInternalUrl = vi.fn(); +const mockBuildForwardHeaders = vi.fn(); +const mockFetch = vi.fn(); + +vi.mock("@/lib/mnote-web/internal-url", () => ({ + resolveMnoteWebInternalUrl: (...args: unknown[]) => mockResolveMnoteWebInternalUrl(...args), +})); + +vi.mock("@/lib/server/forward-headers", () => ({ + buildForwardHeaders: (...args: unknown[]) => mockBuildForwardHeaders(...args), +})); + +describe("/api/tree/shell route", () => { + beforeEach(() => { + mockResolveMnoteWebInternalUrl.mockReset(); + mockBuildForwardHeaders.mockReset(); + mockFetch.mockReset(); + vi.stubGlobal("fetch", mockFetch); + }); + + it("通过 3000 同源代理回源 mnote-web tree shell,并移除不应透传的响应头", async () => { + mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); + mockBuildForwardHeaders.mockResolvedValue( + new Headers({ + cookie: "session=abc", + authorization: "Bearer test-token", + }), + ); + mockFetch.mockResolvedValue( + new Response("tree shell", { + status: 200, + headers: { + "content-type": "text/html; charset=utf-8", + "set-cookie": "debug=1", + connection: "keep-alive", + "x-upstream": "mnote-web-tree", + }, + }), + ); + + const { GET } = await import("./route"); + const response = await GET( + new Request( + "http://127.0.0.1:3000/api/tree/shell?workspaceId=ws_1&mode=page&activeDocumentId=doc_9", + { + method: "GET", + headers: { + cookie: "session=abc", + }, + }, + ), + ); + + expect(mockFetch).toHaveBeenCalledWith( + "http://127.0.0.1:3104/tree?workspaceId=ws_1&mode=page&activeDocumentId=doc_9", + expect.objectContaining({ + method: "GET", + headers: expect.any(Headers), + cache: "no-store", + redirect: "manual", + }), + ); + + const fetchHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers; + expect(fetchHeaders.get("cookie")).toBe("session=abc"); + expect(fetchHeaders.get("authorization")).toBe("Bearer test-token"); + expect(fetchHeaders.get("accept")).toContain("text/html"); + expect(fetchHeaders.get("x-mnote-source-channel")).toBe("next_tree_shell_proxy"); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/html"); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("set-cookie")).toBeNull(); + expect(response.headers.get("connection")).toBeNull(); + expect(response.headers.get("x-upstream")).toBe("mnote-web-tree"); + expect(await response.text()).toContain("tree shell"); + }); +}); diff --git a/wolai-frontend/src/app/api/tree/shell/route.ts b/wolai-frontend/src/app/api/tree/shell/route.ts new file mode 100644 index 00000000..feaf429a --- /dev/null +++ b/wolai-frontend/src/app/api/tree/shell/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from "next/server"; +import { resolveMnoteWebInternalUrl } from "@/lib/mnote-web/internal-url"; +import { buildForwardHeaders } from "@/lib/server/forward-headers"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +const stripHopByHopHeaders = (headers: Headers) => { + // 说明:代理响应不应继续透传 hop-by-hop headers,避免浏览器拿到无效连接语义。 + const hopByHopHeaders = [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "content-length", + ]; + hopByHopHeaders.forEach((name) => headers.delete(name)); +}; + +export async function GET(request: Request) { + try { + const requestUrl = new URL(request.url); + const internalBaseUrl = await resolveMnoteWebInternalUrl(); + const targetUrl = new URL("/tree", `${internalBaseUrl}/`); + targetUrl.search = requestUrl.search; + + const headers = await buildForwardHeaders(request); + headers.set("accept", "text/html,application/xhtml+xml"); + headers.set("x-mnote-source-channel", "next_tree_shell_proxy"); + headers.set("x-mnote-source-client", "wolai-frontend"); + + const workspaceId = requestUrl.searchParams.get("workspaceId")?.trim(); + if (workspaceId && !headers.has("x-mnote-workspace-id")) { + headers.set("x-mnote-workspace-id", workspaceId); + } + + const upstream = await fetch(targetUrl.toString(), { + method: "GET", + headers, + cache: "no-store", + redirect: "manual", + }); + const body = await upstream.arrayBuffer(); + const responseHeaders = new Headers(upstream.headers); + stripHopByHopHeaders(responseHeaders); + responseHeaders.delete("set-cookie"); + responseHeaders.set("cache-control", "no-store"); + + return new NextResponse(body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "tree shell 代理失败"; + return NextResponse.json( + { + error: message, + }, + { status: 502 }, + ); + } +} diff --git a/wolai-frontend/src/components/app-layout-shell.tsx b/wolai-frontend/src/components/app-layout-shell.tsx index 8aa93ead..874a45d3 100644 --- a/wolai-frontend/src/components/app-layout-shell.tsx +++ b/wolai-frontend/src/components/app-layout-shell.tsx @@ -23,6 +23,7 @@ export function AppLayoutShell({ initialData, children }: AppLayoutShellProps) { initialData, sidebarQueryData: sidebarQuery.data, treeStreamData: treeStream.data, + treeStreamStatus: treeStream.status, }); return ( diff --git a/wolai-frontend/src/components/documents/move-embed-picker-dialog.test.tsx b/wolai-frontend/src/components/documents/move-embed-picker-dialog.test.tsx index a4216528..8cea008b 100644 --- a/wolai-frontend/src/components/documents/move-embed-picker-dialog.test.tsx +++ b/wolai-frontend/src/components/documents/move-embed-picker-dialog.test.tsx @@ -39,12 +39,14 @@ vi.mock("@tanstack/react-query", () => ({ }, })); +const mockUseDocumentSearch = vi.fn(() => ({ + data: null, + isLoading: false, + error: null, +})); + vi.mock("@/hooks/use-document-search", () => ({ - useDocumentSearch: () => ({ - data: null, - isLoading: false, - error: null, - }), + useDocumentSearch: (...args: unknown[]) => mockUseDocumentSearch(...args), })); vi.mock("@/components/ui/dialog", () => ({ @@ -100,6 +102,13 @@ describe("MoveEmbedPickerDialog", () => { }); container.remove(); vi.clearAllMocks(); + mockUseDocumentSearch.mockReset(); + mockUseDocumentSearch.mockReturnValue({ + data: null, + isLoading: false, + error: null, + }); + delete window.__MNOTE_RUNTIME_CONFIG__; }); it("空查询时会使用统一 picker surface 并透传根目录选择", async () => { @@ -131,4 +140,130 @@ describe("MoveEmbedPickerDialog", () => { expect(onPick).toHaveBeenCalledWith("move", null); expect(onOpenChange).toHaveBeenCalledWith(false); }); + + it("搜索结果也应继续复用统一 picker surface", async () => { + mockUseDocumentSearch.mockReturnValue({ + data: { + results: [ + { + id: "doc_target", + title: "目标页面", + matchField: "title", + }, + ], + }, + isLoading: false, + error: null, + }); + + const onPick = vi.fn(async () => undefined); + const onOpenChange = vi.fn(); + + await act(async () => { + root.render( + , + ); + }); + + const input = container.querySelector("input"); + expect(input).not.toBeNull(); + + await act(async () => { + input?.dispatchEvent(new Event("input", { bubbles: true })); + Object.defineProperty(input as HTMLInputElement, "value", { + configurable: true, + value: "目标", + }); + input?.dispatchEvent(new Event("change", { bubbles: true })); + }); + + const pickerSurface = container.querySelector('[data-testid="tree-picker-surface"]'); + const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]'); + expect(pickerSurface).not.toBeNull(); + expect(pickerRows).toHaveLength(1); + + await act(async () => { + pickerRows[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(onPick).toHaveBeenCalledWith("move", "doc_target"); + expect(onOpenChange).toHaveBeenCalledWith(false); + }); + + it("rust_family 配置下,picker 空态与结果态都应进入统一 host", async () => { + window.__MNOTE_RUNTIME_CONFIG__ = { + treeRendererFamily: "rust_family", + }; + + const onPick = vi.fn(async () => undefined); + const onOpenChange = vi.fn(); + + await act(async () => { + root.render( + , + ); + }); + + const emptySurface = container.querySelector('[data-testid="tree-picker-surface"]'); + expect(emptySurface?.getAttribute("data-tree-host-kind")).toBe("rust_family"); + expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy"); + expect( + container.querySelector('[data-testid="tree-picker-surface-rust-host"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'), + ).not.toBeNull(); + + mockUseDocumentSearch.mockReturnValue({ + data: { + results: [ + { + id: "doc_target", + title: "目标页面", + matchField: "title", + }, + ], + }, + isLoading: false, + error: null, + }); + + const input = container.querySelector("input"); + expect(input).not.toBeNull(); + + await act(async () => { + Object.defineProperty(input as HTMLInputElement, "value", { + configurable: true, + value: "目标", + }); + input?.dispatchEvent(new Event("input", { bubbles: true })); + input?.dispatchEvent(new Event("change", { bubbles: true })); + }); + + const resultSurface = container.querySelector('[data-testid="tree-picker-surface"]'); + expect(resultSurface?.getAttribute("data-tree-host-kind")).toBe("rust_family"); + expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy"); + expect( + container.querySelector('[data-testid="tree-picker-surface-rust-host"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'), + ).not.toBeNull(); + }); }); diff --git a/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx b/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx index d69cb972..0c2c71dc 100644 --- a/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx +++ b/wolai-frontend/src/components/documents/move-embed-picker-dialog.tsx @@ -8,8 +8,8 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { SidebarInitialData } from "@/components/sidebar/types"; +import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; import { buildPageTreeProjectionItems, buildPickerTreeItems } from "@/lib/tree-projection"; -import { cn } from "@/lib/utils"; import { useDocumentSearch } from "@/hooks/use-document-search"; import type { DocumentSearchFilters, DocumentSearchResult } from "@/types/search"; @@ -106,6 +106,7 @@ function MoveEmbedPickerDialogBody({ const [mode, setMode] = useState(defaultMode); const [query, setQuery] = useState(""); const [highlighted, setHighlighted] = useState(0); + const treeRendererFamily = getMnoteRuntimeConfig().treeRendererFamily ?? "react"; const handleModeChange = useCallback((value: string) => { setMode(value as MoveEmbedMode); @@ -207,6 +208,11 @@ function MoveEmbedPickerDialogBody({
没有匹配结果
) : ( 没有匹配结果 ) : ( -
- {items.map((item, idx) => ( - - ))} -
+ { + void handlePick(targetId); + }} + /> )} diff --git a/wolai-frontend/src/components/sidebar/sidebar-sync.ts b/wolai-frontend/src/components/sidebar/sidebar-sync.ts index 099ee3a2..9923caa9 100644 --- a/wolai-frontend/src/components/sidebar/sidebar-sync.ts +++ b/wolai-frontend/src/components/sidebar/sidebar-sync.ts @@ -1,6 +1,7 @@ import type { SidebarInitialData } from "@/components/sidebar/types"; import type { SidebarTreeNode } from "@/lib/kernel-sidebar"; import type { MediaAsset } from "@/types/media"; +import type { KernelFileTreeProjection } from "@/lib/kernel-file-tree"; type SidebarTreeSnapshot = { id: string; @@ -42,6 +43,20 @@ function toMillis(value: string | null | undefined): number { return Number.isFinite(parsed) ? parsed : 0; } +function normalizeKernelFileTreeProjection( + projection: SidebarInitialData["kernelFileTreeProjection"] | undefined, +): KernelFileTreeProjection { + return ( + projection ?? { + projectionId: "kernel_projection:file_tree:missing", + projection: "file_tree", + rootNodeId: null, + items: [], + edges: [], + } + ); +} + export function buildSidebarTreeSyncKey(nodes: SidebarTreeNode[]): string { return JSON.stringify(toSidebarTreeSnapshot(nodes)); } @@ -51,9 +66,31 @@ export function buildMediaAssetListSyncKey(assets: MediaAsset[]): string { } export function buildSidebarDataSyncKey(data: SidebarInitialData): string { + const fileTreeProjection = normalizeKernelFileTreeProjection(data.kernelFileTreeProjection); return JSON.stringify({ activeWorkspaceId: data.activeWorkspaceId, tree: toSidebarTreeSnapshot(data.kernelSidebarTree), + fileTree: { + projectionId: fileTreeProjection.projectionId, + rootNodeId: fileTreeProjection.rootNodeId, + items: fileTreeProjection.items.map((item) => ({ + rowId: item.rowId, + rowKind: item.rowKind, + nodeId: item.nodeId, + parentNodeId: item.parentNodeId, + title: item.title, + depth: item.depth, + position: item.position, + childCount: item.childCount, + expandable: item.expandable, + expandedByDefault: item.expandedByDefault, + iconHint: item.iconHint ?? null, + resourceKind: item.resourceMeta.resourceKind, + documentId: item.resourceMeta.documentId ?? null, + assetId: item.resourceMeta.assetId ?? null, + assetKind: item.resourceMeta.assetKind ?? null, + })), + }, mediaAssets: toMediaAssetSnapshot(data.mediaAssets ?? []), mindmapAssets: toMediaAssetSnapshot(data.mindmapAssets ?? []), tableAssets: toMediaAssetSnapshot(data.tableAssets ?? []), @@ -73,6 +110,8 @@ export function buildSidebarDataSyncKey(data: SidebarInitialData): string { export function getSidebarDataFreshness(data: SidebarInitialData): number { const documentTimes = data.documents.map((item) => toMillis(item.updated_at ?? item.created_at)); const treeTimes = data.kernelSidebarTree.map((item) => toMillis(item.updated_at ?? item.created_at)); + const fileTreeProjection = normalizeKernelFileTreeProjection(data.kernelFileTreeProjection); + const fileTreeTimes = fileTreeProjection.items.map((item) => item.position ?? 0); const assetTimes = [ ...(data.mediaAssets ?? []), ...(data.mindmapAssets ?? []), @@ -87,6 +126,7 @@ export function getSidebarDataFreshness(data: SidebarInitialData): number { 0, ...documentTimes, ...treeTimes, + ...fileTreeTimes, ...assetTimes, ...trashedDocumentTimes, ); diff --git a/wolai-frontend/src/components/sidebar/sidebar.tsx b/wolai-frontend/src/components/sidebar/sidebar.tsx index 8f508dbf..ff0e07eb 100644 --- a/wolai-frontend/src/components/sidebar/sidebar.tsx +++ b/wolai-frontend/src/components/sidebar/sidebar.tsx @@ -199,6 +199,7 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar initialData, sidebarQueryData: sidebarQuery.data, treeStreamData: treeStream.data, + treeStreamStatus: treeStream.status, }); const sidebarData = externalSidebarData ?? preferredSidebarSnapshot.data; @@ -211,6 +212,7 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar const { signOut } = useAuthActions(); const activeId = segments?.[1] ?? ""; const editorBridge = useEditorBridgeStore((state) => state.bridge); + const treeRendererFamily = getMnoteRuntimeConfig().treeRendererFamily ?? "react"; const [tree, setTree] = useState(() => sidebarData.kernelSidebarTree); const [filter, setFilter] = useState(""); @@ -673,18 +675,28 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar const resourceRows = useMemo( () => buildVisibleRows({ + fileTreeItems: + filter.trim().length === 0 + ? sidebarData.kernelFileTreeProjection.items + : undefined, pageRows: visibleFilteredPrivatePageRows, expanded, assetsByDoc, assetChildrenByAssetId: mindmapChildrenSnapshot.childAssetsByMindmapId, expandedAssetFolderIds: expandedAssetFolders, + nodeById, + assetById, }), [ + assetById, assetsByDoc, expanded, expandedAssetFolders, mindmapChildrenSnapshot.childAssetsByMindmapId, + nodeById, + sidebarData.kernelFileTreeProjection.items, visibleFilteredPrivatePageRows, + filter, ], ); @@ -967,6 +979,118 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar [], ); + const handlePageTreeShellNavigate = useCallback( + (documentId: string) => { + handleOpenDocument(documentId, "sidebar"); + }, + [handleOpenDocument], + ); + + const handleFileTreeShellNavigate = useCallback( + (documentId: string) => { + handleOpenDocument(documentId, "main"); + }, + [handleOpenDocument], + ); + + const handlePageTreeShellContextMenu = useCallback( + (payload: { documentId: string; x: number; y: number }) => { + const node = nodeById.get(payload.documentId); + if (!node) { + return; + } + setContextMenu({ + node, + x: payload.x, + y: payload.y, + }); + }, + [nodeById], + ); + + const handleFileTreeShellContextMenu = useCallback( + (payload: { + documentId: string | null; + assetId: string | null; + rowId: string | null; + rowKind: string | null; + x: number; + y: number; + }) => { + if (payload.assetId) { + const asset = assetById.get(payload.assetId); + if (asset) { + setAssetMenu({ + asset, + x: payload.x, + y: payload.y, + }); + return; + } + } + + const row = payload.rowId ? resourceRowById.get(payload.rowId) : null; + const node = + row && (row.kind === "doc" || row.kind === "index") + ? row.node + : payload.documentId + ? nodeById.get(payload.documentId) ?? null + : null; + if (!node) { + return; + } + setContextMenu({ + node, + x: payload.x, + y: payload.y, + }); + }, + [assetById, nodeById, resourceRowById], + ); + + const handleFileTreeShellSelectionChange = useCallback( + (payload: { + selectedRowIds: string[]; + anchorRowId: string | null; + focusedRowId: string | null; + }) => { + const visibleRowIds = resourceRows.map((row) => row.rowId); + const normalized = normalizeTreePaneSelectionForVisibleRows( + { + selectedRowIds: new Set( + payload.selectedRowIds.filter((rowId) => resourceRowById.has(rowId)), + ), + anchorRowId: + payload.anchorRowId && resourceRowById.has(payload.anchorRowId) + ? payload.anchorRowId + : null, + focusedRowId: + payload.focusedRowId && resourceRowById.has(payload.focusedRowId) + ? payload.focusedRowId + : null, + }, + visibleRowIds, + ); + setResourceSelection(normalized); + }, + [resourceRowById, resourceRows], + ); + + const handleFileTreeShellAssetOpen = useCallback( + (payload: { assetId: string; documentId: string | null }) => { + const asset = assetById.get(payload.assetId); + if (!asset) { + return; + } + handleOpenAsset(asset); + }, + [assetById, handleOpenAsset], + ); + + const handleTreeShellMutation = useCallback(() => { + void refreshTree(); + }, [refreshTree]); + useEffect(() => { const handler = async (event: KeyboardEvent) => { const isCopy = @@ -2593,6 +2717,9 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar
) : ( @@ -2618,6 +2748,9 @@ function SidebarContent({ initialData, sidebarData: externalSidebarData, sidebar > diff --git a/wolai-frontend/src/components/sidebar/tree-shell-host.tsx b/wolai-frontend/src/components/sidebar/tree-shell-host.tsx new file mode 100644 index 00000000..10042320 --- /dev/null +++ b/wolai-frontend/src/components/sidebar/tree-shell-host.tsx @@ -0,0 +1,137 @@ +"use client"; + +import type { ReactNode } from "react"; +import { + TreeShellIframeHost, + type TreeShellPickerItem, +} from "@/components/sidebar/tree-shell-iframe-host"; +import type { FileTreeRow } from "@/lib/file-tree/types"; +import { cn } from "@/lib/utils"; + +export type TreeRendererFamily = "react" | "rust_family"; + +export type TreeShellHostMode = "page" | "filetree" | "picker"; + +type TreeShellHostProps = { + mode: TreeShellHostMode; + surfaceTestId: string; + rendererFamily?: TreeRendererFamily; + className?: string; + treeShellEnabled?: boolean; + workspaceId?: string | null; + rootNodeId?: string | null; + activeDocumentId?: string | null; + allowRootPick?: boolean; + excludeIds?: string[]; + pickerItems?: TreeShellPickerItem[]; + fileTreeRows?: FileTreeRow[]; + channel?: string; + host?: string; + onNavigate?: (documentId: string) => void; + onPick?: (targetId: string | null) => void; + onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void; + onFileTreeContextMenu?: (payload: { + documentId: string | null; + assetId: string | null; + rowId: string | null; + rowKind: string | null; + x: number; + y: number; + }) => void; + onFileTreeSelectionChange?: (payload: { + selectedRowIds: string[]; + anchorRowId: string | null; + focusedRowId: string | null; + }) => void; + onInternalDrop?: (args: { targetRow: FileTreeRow; rowIds: string[]; copy: boolean }) => void; + onDropFiles?: (docId: string, files: FileList | File[], targetRow?: FileTreeRow) => void; + onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void; + onTreeMutation?: (payload: { type: string; documentId: string | null }) => void; + children: ReactNode; +}; + +export function TreeShellHost({ + mode, + surfaceTestId, + rendererFamily = "react", + className, + treeShellEnabled = true, + workspaceId = null, + rootNodeId = null, + activeDocumentId = null, + allowRootPick = false, + excludeIds = [], + pickerItems = [], + fileTreeRows = [], + channel, + host, + onNavigate, + onPick, + onPageContextMenu, + onFileTreeContextMenu, + onFileTreeSelectionChange, + onInternalDrop, + onDropFiles, + onAssetOpen, + onTreeMutation, + children, +}: TreeShellHostProps) { + const useRustHost = rendererFamily === "rust_family"; + const useIframeHost = useRustHost && treeShellEnabled && Boolean(workspaceId?.trim()); + const hostKind = rendererFamily === "rust_family" ? "rust_family" : "react"; + const implementation = useIframeHost + ? "mnote_web_iframe_proxy" + : rendererFamily === "rust_family" + ? "react_fallback" + : "react_primary"; + + return ( +
+ {useRustHost ? ( +
+ {useIframeHost && workspaceId ? ( + + ) : ( + children + )} +
+ ) : ( + children + )} +
+ ); +} diff --git a/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.test.tsx b/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.test.tsx new file mode 100644 index 00000000..95ae17fa --- /dev/null +++ b/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.test.tsx @@ -0,0 +1,304 @@ +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + TreeShellIframeHost, + buildTreeShellIframeSrc, + buildTreeShellInlinePickerItems, + injectTreeShellInlineOverrides, +} from "./tree-shell-iframe-host"; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +describe("tree-shell-iframe-host", () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + it("应构造同源 tree shell iframe 地址,并透传 picker 所需 query", () => { + const src = buildTreeShellIframeSrc({ + mode: "picker", + workspaceId: "ws_picker", + activeDocumentId: "doc_active", + allowRootPick: true, + excludeIds: ["doc_hidden", "doc_other"], + channel: "tree-picker-surface", + host: "tree-picker-surface", + }); + const url = new URL(src, "http://127.0.0.1:3000"); + + expect(url.pathname).toBe("/api/tree/shell"); + expect(url.searchParams.get("workspaceId")).toBe("ws_picker"); + expect(url.searchParams.get("mode")).toBe("picker"); + expect(url.searchParams.get("activeDocumentId")).toBe("doc_active"); + expect(url.searchParams.get("allowRootPick")).toBe("1"); + expect(url.searchParams.get("excludeIds")).toBe("doc_hidden,doc_other"); + expect(url.searchParams.get("channel")).toBe("tree-picker-surface"); + expect(url.searchParams.get("host")).toBe("tree-picker-surface"); + }); + + it("应能把 picker 搜索结果转换为 inline shell items 并注入到 HTML", () => { + const pickerItems = buildTreeShellInlinePickerItems([ + { kind: "doc", id: "doc_target", title: "目标页面", depth: 0 }, + { kind: "doc", id: "doc_recent", title: "最近打开", depth: 0 }, + ]); + + expect(pickerItems).toEqual([ + expect.objectContaining({ + nodeId: "doc_target", + parentNodeId: null, + title: "目标页面", + depth: 0, + childCount: 0, + }), + expect.objectContaining({ + nodeId: "doc_recent", + parentNodeId: null, + title: "最近打开", + depth: 0, + childCount: 0, + }), + ]); + + const html = injectTreeShellInlineOverrides( + '', + { items: pickerItems }, + ); + + expect(html).toContain("__MNOTE_TREE_SHELL_OVERRIDE__"); + expect(html).toContain("\\u003c/script\\u003e"); + expect(html.indexOf("__MNOTE_TREE_SHELL_OVERRIDE__")).toBeGreaterThan(-1); + expect(html.indexOf("__MNOTE_TREE_SHELL_OVERRIDE__")).toBeLessThan( + html.indexOf('id="tree-shell-state"'), + ); + }); + + it("应把 iframe postMessage 桥接回宿主回调,并忽略错误 channel", async () => { + const onNavigate = vi.fn(); + const onPageContextMenu = vi.fn(); + const onPick = vi.fn(); + const onFileTreeContextMenu = vi.fn(); + const onFileTreeSelectionChange = vi.fn(); + const onAssetOpen = vi.fn(); + const onTreeMutation = vi.fn(); + const onInternalDrop = vi.fn(); + const onDropFiles = vi.fn(); + const targetRow = { + kind: "doc", + rowId: "doc:doc_target", + depth: 0, + docId: "doc_target", + parentDocId: null, + node: { + _id: "doc_target", + id: "doc_target", + title: "目标页面", + }, + hasChildren: false, + isExpanded: false, + }; + const droppedFile = new File(["hello"], "hello.txt", { type: "text/plain" }); + + await act(async () => { + root.render( + , + ); + }); + + const iframe = container.querySelector("iframe"); + expect(iframe).not.toBeNull(); + Object.defineProperty(iframe, "contentWindow", { + configurable: true, + value: window, + }); + + await act(async () => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "wrong-channel", + type: "tree.navigate", + documentId: "doc_wrong", + }, + source: window, + }), + ); + }); + + expect(onNavigate).not.toHaveBeenCalled(); + + await act(async () => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.navigate", + documentId: "doc_2", + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.page.context-menu", + documentId: "doc_2", + x: 12, + y: 34, + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.pick.root", + documentId: null, + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.filetree.context-menu", + documentId: "doc_2", + assetId: "asset_2", + rowId: "asset:asset_2", + rowKind: "asset", + x: 56, + y: 78, + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.filetree.selection.changed", + selectedRowIds: ["doc:doc_2", "index:doc_2"], + anchorRowId: "doc:doc_2", + focusedRowId: "index:doc_2", + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.asset.open", + documentId: "doc_2", + assetId: "asset_2", + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.node.renamed", + documentId: "doc_2", + }, + source: window, + }), + ); + }); + + expect(onNavigate).toHaveBeenCalledWith("doc_2"); + expect(onPageContextMenu).toHaveBeenCalledWith({ + documentId: "doc_2", + x: 12, + y: 34, + }); + expect(onPick).toHaveBeenCalledWith(null); + expect(onFileTreeContextMenu).toHaveBeenCalledWith({ + documentId: "doc_2", + assetId: "asset_2", + rowId: "asset:asset_2", + rowKind: "asset", + x: 56, + y: 78, + }); + expect(onFileTreeSelectionChange).toHaveBeenCalledWith({ + selectedRowIds: ["doc:doc_2", "index:doc_2"], + anchorRowId: "doc:doc_2", + focusedRowId: "index:doc_2", + }); + expect(onAssetOpen).toHaveBeenCalledWith({ + documentId: "doc_2", + assetId: "asset_2", + }); + expect(onTreeMutation).toHaveBeenCalledWith({ + type: "tree.node.renamed", + documentId: "doc_2", + }); + + await act(async () => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.filetree.internal-drop", + rowIds: ["doc:doc_source", "asset:asset_source"], + copy: true, + targetRow, + }, + source: window, + }), + ); + window.dispatchEvent( + new MessageEvent("message", { + data: { + channel: "sidebar-file-tree-shell", + type: "tree.filetree.drop-files", + documentId: "doc_target", + targetRow, + files: [droppedFile], + }, + source: window, + }), + ); + }); + + expect(onInternalDrop).toHaveBeenCalledWith({ + targetRow, + rowIds: ["doc:doc_source", "asset:asset_source"], + copy: true, + }); + expect(onDropFiles).toHaveBeenCalledWith("doc_target", [droppedFile], targetRow); + }); +}); diff --git a/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx b/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx new file mode 100644 index 00000000..884df6df --- /dev/null +++ b/wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx @@ -0,0 +1,559 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import type { TreeShellHostMode } from "@/components/sidebar/tree-shell-host"; +import type { FileTreeRow } from "@/lib/file-tree/types"; + +type TreeShellDocRow = Extract; +type TreeShellIndexRow = Extract; +type TreeShellAssetFolderRow = Extract; +type TreeShellAssetRow = Extract; + +type TreeShellBridgeContextMenuPayload = { + documentId: string | null; + assetId: string | null; + rowId: string | null; + rowKind: string | null; + x: number; + y: number; +}; + +type TreeShellBridgeMutationPayload = { + type: string; + documentId: string | null; +}; + +type TreeShellBridgeSelectionPayload = { + selectedRowIds: string[]; + anchorRowId: string | null; + focusedRowId: string | null; +}; + +export type TreeShellPickerItem = + | { kind: "root"; id: null; title: string; subtitle?: string; depth?: number } + | { kind: "doc"; id: string; title: string; subtitle?: string; depth?: number }; + +type TreeShellInlineProjectionItem = { + nodeId: string; + parentNodeId: string | null; + title: string; + depth: number; + childCount: number; + position: number; + expandedByDefault: boolean; +}; + +export type TreeShellIframeHostProps = { + mode: TreeShellHostMode; + surfaceTestId: string; + workspaceId: string; + rootNodeId?: string | null; + activeDocumentId?: string | null; + allowRootPick?: boolean; + excludeIds?: string[]; + pickerItems?: TreeShellPickerItem[]; + fileTreeRows?: FileTreeRow[]; + channel?: string; + host?: string; + onNavigate?: (documentId: string) => void; + onPick?: (targetId: string | null) => void; + onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void; + onFileTreeContextMenu?: (payload: TreeShellBridgeContextMenuPayload) => void; + onFileTreeSelectionChange?: (payload: TreeShellBridgeSelectionPayload) => void; + onInternalDrop?: (args: { targetRow: FileTreeRow; rowIds: string[]; copy: boolean }) => void; + onDropFiles?: (docId: string, files: FileList | File[], targetRow?: FileTreeRow) => void; + onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void; + onTreeMutation?: (payload: TreeShellBridgeMutationPayload) => void; +}; + +type TreeShellBridgeMessage = { + channel: string; + type: string; + documentId: string | null; + assetId: string | null; + rowId: string | null; + rowKind: string | null; + x: number; + y: number; + selectedRowIds: string[]; + anchorRowId: string | null; + focusedRowId: string | null; + rowIds: string[]; + copy: boolean; + targetRow: FileTreeRow | null; + files: File[]; +}; + +const INLINE_TREE_SHELL_LOADING_HTML = [ + "", + '', + "", + ' ', + ' ', + " Tree Shell Loading", + " ", + "", + "", + "

正在加载树结果…

", + "", + "", +].join(""); + +const normalizeString = (value: unknown, fallback = "") => { + if (typeof value !== "string") return fallback; + const normalized = value.trim(); + return normalized || fallback; +}; + +const readNullableString = (value: unknown) => { + const normalized = normalizeString(value); + return normalized || null; +}; + +const readNumber = (value: unknown) => { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +}; + +const readBoolean = (value: unknown) => value === true; + +const isRecord = (value: unknown): value is Record => { + return Boolean(value) && typeof value === "object"; +}; + +const readStringArray = (value: unknown) => { + if (!Array.isArray(value)) { + return []; + } + return value + .map((item) => normalizeString(item)) + .filter(Boolean); +}; + +const readFiles = (value: unknown) => { + if (typeof FileList !== "undefined" && value instanceof FileList) { + return Array.from(value); + } + if (!Array.isArray(value)) { + return []; + } + return value.filter((item): item is File => typeof File !== "undefined" && item instanceof File); +}; + +const readFileTreeRow = (value: unknown): FileTreeRow | null => { + if (!isRecord(value)) { + return null; + } + + const kind = normalizeString(value.kind); + const rowId = normalizeString(value.rowId); + const docId = normalizeString(value.docId); + const depth = Math.max(0, readNumber(value.depth)); + + if (!kind || !rowId || !docId) { + return null; + } + + switch (kind) { + case "doc": + if (!isRecord(value.node)) { + return null; + } + return { + kind: "doc", + rowId: rowId as FileTreeRow["rowId"], + depth, + docId, + parentDocId: readNullableString(value.parentDocId), + node: value.node as TreeShellDocRow["node"], + hasChildren: readBoolean(value.hasChildren), + isExpanded: readBoolean(value.isExpanded), + }; + case "index": + if (!isRecord(value.node)) { + return null; + } + return { + kind: "index", + rowId: rowId as FileTreeRow["rowId"], + depth, + docId, + parentDocId: readNullableString(value.parentDocId) ?? docId, + node: value.node as TreeShellIndexRow["node"], + }; + case "asset-folder": + if (!isRecord(value.asset)) { + return null; + } + return { + kind: "asset-folder", + rowId: rowId as FileTreeRow["rowId"], + depth, + docId, + parentDocId: readNullableString(value.parentDocId) ?? docId, + asset: value.asset as TreeShellAssetFolderRow["asset"], + hasChildren: readBoolean(value.hasChildren), + isExpanded: readBoolean(value.isExpanded), + }; + case "asset": + if (!isRecord(value.asset)) { + return null; + } + return { + kind: "asset", + rowId: rowId as FileTreeRow["rowId"], + depth, + docId, + parentDocId: readNullableString(value.parentDocId) ?? docId, + asset: value.asset as TreeShellAssetRow["asset"], + }; + default: + return null; + } +}; + +function parseTreeShellBridgeMessage( + value: unknown, + expectedChannel: string, +): TreeShellBridgeMessage | null { + if (!value || typeof value !== "object") { + return null; + } + const payload = value as Record; + const channel = normalizeString(payload.channel); + const type = normalizeString(payload.type); + if (!channel || !type || channel !== expectedChannel) { + return null; + } + return { + channel, + type, + documentId: readNullableString(payload.documentId ?? payload.docId), + assetId: readNullableString(payload.assetId), + rowId: readNullableString(payload.rowId ?? payload.targetRowId), + rowKind: readNullableString(payload.rowKind ?? payload.targetRowKind), + x: readNumber(payload.x), + y: readNumber(payload.y), + selectedRowIds: readStringArray(payload.selectedRowIds), + anchorRowId: readNullableString(payload.anchorRowId), + focusedRowId: readNullableString(payload.focusedRowId), + rowIds: readStringArray(payload.rowIds), + copy: readBoolean(payload.copy), + targetRow: readFileTreeRow(payload.targetRow), + files: readFiles(payload.files), + }; +} + +const escapeInlineScriptJson = (input: string) => { + return input + .replace(/&/g, "\\u0026") + .replace(//g, "\\u003e"); +}; + +export function buildTreeShellInlinePickerItems( + items: TreeShellPickerItem[], +): TreeShellInlineProjectionItem[] { + return items + .filter( + (item): item is Extract => + item.kind === "doc" && Boolean(normalizeString(item.id)), + ) + .map((item, index) => ({ + nodeId: normalizeString(item.id), + parentNodeId: null, + title: normalizeString(item.title, "无标题"), + depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0, + childCount: 0, + position: index, + expandedByDefault: false, + })); +} + +export function injectTreeShellInlineOverrides( + html: string, + overrides: { items?: TreeShellInlineProjectionItem[] }, +) { + const payloadJson = escapeInlineScriptJson(JSON.stringify(overrides)); + const scriptTag = ``; + const anchor = '