From 10799b2f2ec6d1be69d2bcc197624b12b0adcdf6 Mon Sep 17 00:00:00 2001 From: Tava Date: Tue, 7 Jul 2026 11:27:52 -0400 Subject: [PATCH] feat(loadfile): request directory contents via new iframe API (closes #9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ouroboros #106 stopped broadcasting the recursive file tree in send-directory-contents, so the plugin's LoadFile modal has been rendering an empty list. ouroboros #114 (merged) added a request-directory-contents / send-directory-contents-response pair scoped per requesting iframe. Wire it up in App: - On send-directory-contents (root change), dispatch a request-directory-contents with recursive:true and a monotonic requestId (loadfile-N). - Track the active requestId in a ref; drop responses whose id does not match, so late responses from a previous root can't clobber current state. - On send-directory-contents-response, merge data.nodes back into directoryData so LoadFile's existing tree renderer sees the tree again without other changes. - Error handling: denied / not-found / internal → empty tree (matches the existing 'no files' rendering); limit → render the partial nodes the response carried (truncated hint deferred as follow-up). - Guard the merge with a path check so an out-of-order response for a previous root can't overwrite the new root's tree. LoadFile itself unchanged. Verified: tsc -b, npm run build. Manual UI smoke against a running Ouroboros not run here — see PR body. --- src/App/index.tsx | 79 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/App/index.tsx b/src/App/index.tsx index c081720..a3e3d0c 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -50,6 +50,12 @@ function App(): JSX.Element { null ) + // The most recent directory-contents request we issued. Responses whose + // requestId does not match are stale (e.g. from a previous root) and + // ignored to prevent overwriting fresh state with older data. + const activeRequestIdRef = useRef(null) + const requestCounterRef = useRef(0) + const [showModal, setShowModal] = useState(false) const [form, setForm] = useState("") @@ -180,9 +186,80 @@ function App(): JSX.Element { const result = event.data switch (result.type) { - case "send-directory-contents": + case "send-directory-contents": { + // Root-change broadcast: carries directoryPath / directoryName + // but ships an empty nodes tree since ouroboros #106. Preserve + // the metadata, then request the recursive tree separately. setDirectoryData(result) + + const nextPath: string | null = + result?.data?.directoryPath ?? null + if (!nextPath) { + activeRequestIdRef.current = null + break + } + + const requestId = `loadfile-${++requestCounterRef.current}` + activeRequestIdRef.current = requestId + + parent.postMessage( + { + type: "request-directory-contents", + data: { + path: nextPath, + recursive: true, + requestId, + }, + }, + "*" + ) break + } + case "send-directory-contents-response": { + // Late responses from a previous root arrive after we've moved + // on; drop them. requestId is nullable in the schema, so a + // missing id also fails to match. + if (result?.data?.requestId !== activeRequestIdRef.current) { + break + } + + const responsePath: string = result.data.path + const responseNodes: NodeChildren = + result.data.nodes ?? {} + const errorCode: string | undefined = result.data.error?.code + + // denied / not-found / internal → treat as empty tree; the + // user's "no files" state is already the right rendering. + // limit → partial nodes present; render what we got. + // (The truncated flag could surface a hint later; keep the + // current diff focused on unblocking discovery.) + const nextNodes: NodeChildren = + errorCode === "denied" || + errorCode === "not-found" || + errorCode === "internal" + ? {} + : responseNodes + + if (errorCode === "internal") { + console.error( + "request-directory-contents failed:", + result.data.error + ) + } + + setDirectoryData((prev) => { + if (!prev) return prev + if (prev.data.directoryPath !== responsePath) return prev + return { + ...prev, + data: { + ...prev.data, + nodes: nextNodes, + }, + } + }) + break + } case "send-neuroglancer-json": case "read-file-response": if (viewerMode === "ngrefactor") {