From fceb376551093be69e918786532d88eb63a2d07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 11 Aug 2026 01:37:35 -0400 Subject: [PATCH] fix(studio): match the write receipt in dev, so an edit stops reloading the preview (#3206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Editing anything in the canvas on the dev server reloaded the preview iframe. It no longer does. ## Why The write receipt exists to prevent exactly this: Studio marks its own writes so the file-watcher echo can be told apart from somebody editing the file underneath it. The receipt is matched on the file's current bytes as well as its path, so `consumeFileWriteReceipt(absPath, expectedVersion)` takes a version. The dev plugin called it with the path alone. `expectedVersion` was `undefined`, the version comparison never matched, and so every Studio write looked external and reloaded the preview. The CLI server — which is what ships — has always passed the version, so this is dev-server only. ## How The plugin reads the file and passes its version, the same way `studioServer.ts` does, and treats a deletion (no readable bytes) as unmatched. ## Test plan Driven on the dev server against a real composition, with `hf-reload-debug` on: - Before: a drag logged `file-change` with a full external path, then `reload`, then `refreshPlayer`, and the iframe navigated — one reload per edit. - After: the same drag logs `file-change` carrying the write token, then `suppressed: own write token`. Iframe reloads are zero across drag, resize and an inline text edit. - Full studio suite (3727), format and lint green. Found while chasing a flash after every canvas edit. The other half of that flash was Vite's own HMR full-reloading the page, fixed separately in #3163; with both in, the canvas stops flashing. --- packages/studio/vite.config.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/studio/vite.config.ts b/packages/studio/vite.config.ts index 365c7b2c3..97830c5d6 100644 --- a/packages/studio/vite.config.ts +++ b/packages/studio/vite.config.ts @@ -68,11 +68,11 @@ function devProjectApi(): Plugin { createStudioApi: (adapter: ReturnType) => { fetch: (req: Request) => Promise; }; - consumeFileWriteReceipt?: (path: string) => { - path: string; - version: string; - writeToken: string; - } | null; + consumeFileWriteReceipt?: ( + path: string, + expectedVersion: string, + ) => { path: string; version: string; writeToken: string } | null; + fileContentVersion?: (content: string) => string; } | null = null; const getApi = async () => { if (!_api) { @@ -177,7 +177,20 @@ function devProjectApi(): Plugin { ) return; console.log(`[Studio] File changed: ${filePath}`); - const receipt = _studioServerModule?.consumeFileWriteReceipt?.(filePath) ?? null; + // The receipt is matched on the file's current bytes, not just its path, + // so a write is only recognised as ours when the version agrees. Calling + // this without the version could never match, which left every Studio + // write looking external and reloaded the preview on each edit. + let version: string | null = null; + try { + version = + _studioServerModule?.fileContentVersion?.(readFileSync(filePath, "utf-8")) ?? null; + } catch { + // A deletion has no current bytes to match a write receipt against. + } + const receipt = version + ? (_studioServerModule?.consumeFileWriteReceipt?.(filePath, version) ?? null) + : null; server.ws.send({ type: "custom", event: "hf:file-change",