diff --git a/packages/studio-server/src/routes/files.test.ts b/packages/studio-server/src/routes/files.test.ts
index ffd9f61a8c..65ee973f0a 100644
--- a/packages/studio-server/src/routes/files.test.ts
+++ b/packages/studio-server/src/routes/files.test.ts
@@ -425,6 +425,38 @@ describe("registerFileRoutes", () => {
expect(readFileSync(join(projectDir, "index.html"), "utf-8")).toContain("After");
});
+ // Without the receipt the client cannot recognise its own edit in the watcher
+ // broadcast, so it treats it as someone else's write and does a full preview
+ // reload — a visible blank on the stage right after the user typed.
+ it("leaves a write receipt so the patch's own file-change echo is identifiable", async () => {
+ const projectDir = createProjectDir();
+ writeFileSync(projectDir + "/index.html", '
Before
');
+ const app = new Hono();
+ registerFileRoutes(app, createAdapter(projectDir));
+
+ const response = await app.request(
+ "http://localhost/projects/demo/file-mutations/patch-element/index.html",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Hyperframes-Write-Token": "studio-patch-1",
+ },
+ body: JSON.stringify({
+ target: { id: "title" },
+ operations: [{ type: "text-content", property: "textContent", value: "After" }],
+ }),
+ },
+ );
+
+ expect(response.status).toBe(200);
+ expect(consumeFileWriteReceipt(join(projectDir, "index.html"))).toEqual({
+ path: "index.html",
+ version: fileContentVersion(readFileSync(join(projectDir, "index.html"), "utf-8")),
+ writeToken: "studio-patch-1",
+ });
+ });
+
it("applies an ordered element patch batch with one file write", async () => {
const projectDir = createProjectDir();
const original =
diff --git a/packages/studio-server/src/routes/files.ts b/packages/studio-server/src/routes/files.ts
index 3ec64bbff1..63b71a4904 100644
--- a/packages/studio-server/src/routes/files.ts
+++ b/packages/studio-server/src/routes/files.ts
@@ -112,6 +112,7 @@ interface RouteContext {
param: (name: string) => string;
path: string;
query: (name: string) => string | undefined;
+ header: (name: string) => string | undefined;
};
header: (name: string, value: string) => void;
json: (data: unknown, status?: number) => Response;
@@ -399,6 +400,35 @@ export function commitElementPatchBatches(
return { durable: true, files };
}
+/**
+ * Write a mutation result, and leave behind the receipt that claims it.
+ *
+ * The file watcher broadcasts every write, including the ones Studio itself just
+ * asked for. The receipt is what lets the client tell its own echo from an agent
+ * or an editor writing the file behind its back: without one, the client treats
+ * its own edit as an external change and does a full preview reload, which blanks
+ * the stage for a few hundred milliseconds right after the user typed. Every
+ * mutation route writes through here so no route can forget.
+ */
+function writeMutationResult(
+ c: RouteContext,
+ projectDir: string,
+ filePath: string,
+ absPath: string,
+ html: string,
+): { backupPath: string | null; version: string } {
+ const backup = snapshotBeforeWrite(projectDir, absPath);
+ if (backup.error) console.warn(`Failed to create backup for ${filePath}: ${backup.error}`);
+ writeFileSync(absPath, html, "utf-8");
+ const version = fileContentVersion(html);
+ recordFileWriteReceipt(absPath, {
+ path: filePath,
+ version,
+ writeToken: createWriteToken(c.req.header("X-Hyperframes-Write-Token")),
+ });
+ return { backupPath: backupPathForResponse(projectDir, backup.backupPath), version };
+}
+
/** Write `next` to `absPath` only if it differs from `original`, returning a standardized change response. */
function writeIfChanged(
c: RouteContext,
@@ -411,15 +441,13 @@ function writeIfChanged(
if (next === original) {
return c.json({ ok: true, changed: false, content: original, path: filePath });
}
- const backup = snapshotBeforeWrite(projectDir, absPath);
- if (backup.error) console.warn(`Failed to create backup for ${filePath}: ${backup.error}`);
- writeFileSync(absPath, next, "utf-8");
+ const { backupPath } = writeMutationResult(c, projectDir, filePath, absPath, next);
return c.json({
ok: true,
changed: true,
content: next,
path: filePath,
- backupPath: backupPathForResponse(projectDir, backup.backupPath),
+ backupPath,
});
}
@@ -1238,10 +1266,13 @@ async function applyGsapMutations(
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
}
if (changed) {
- const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
- if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
- backupPath = backupPathForResponse(res.project.dir, backup.backupPath);
- writeFileSync(res.absPath, newHtml, "utf-8");
+ backupPath = writeMutationResult(
+ c,
+ res.project.dir,
+ res.filePath,
+ res.absPath,
+ newHtml,
+ ).backupPath;
}
const responsePayload: Record = {
@@ -2623,10 +2654,13 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
version,
});
}
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
- writeFileSync(ctx.absPath, result.html, "utf-8");
- const version = fileContentVersion(result.html);
+ const { version, backupPath } = writeMutationResult(
+ c,
+ ctx.project.dir,
+ ctx.filePath,
+ ctx.absPath,
+ result.html,
+ );
c.header("ETag", version);
return c.json({
ok: true,
@@ -2635,7 +2669,7 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
newId: result.newId,
path: ctx.filePath,
version,
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath),
+ backupPath,
});
});
@@ -2676,16 +2710,20 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
path: ctx.filePath,
});
}
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
- writeFileSync(ctx.absPath, patched, "utf-8");
+ const { backupPath } = writeMutationResult(
+ c,
+ ctx.project.dir,
+ ctx.filePath,
+ ctx.absPath,
+ patched,
+ );
return c.json({
ok: true,
changed: true,
matched,
content: patched,
path: ctx.filePath,
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath),
+ backupPath,
});
});
@@ -2807,16 +2845,20 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
result.error === "grouped elements must share a single parent" ? 422 : 400,
);
}
- const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
- if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
- writeFileSync(ctx.absPath, result.html, "utf-8");
+ const { backupPath } = writeMutationResult(
+ c,
+ ctx.project.dir,
+ ctx.filePath,
+ ctx.absPath,
+ result.html,
+ );
return c.json({
ok: true,
changed: true,
groupId: result.groupId,
content: result.html,
path: ctx.filePath,
- backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath),
+ backupPath,
});
});
diff --git a/packages/studio/src/captions/hooks/useCaptionSync.ts b/packages/studio/src/captions/hooks/useCaptionSync.ts
index 5fdbf80138..1b2037a0f4 100644
--- a/packages/studio/src/captions/hooks/useCaptionSync.ts
+++ b/packages/studio/src/captions/hooks/useCaptionSync.ts
@@ -3,6 +3,7 @@ import { useCaptionStore } from "../store";
import { useMountEffect } from "../../hooks/useMountEffect";
import { trackEvent } from "../../telemetry/client";
import type { CaptionStyle } from "../types";
+import { studioWriteHeaders } from "../../utils/studioFileVersion";
interface CaptionOverrideEntry {
wordId?: string;
@@ -77,7 +78,7 @@ export function useCaptionSync(projectId: string | null) {
fetch(`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`, {
method: "PUT",
- headers: { "Content-Type": "text/plain" },
+ headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
body: JSON.stringify(overrides, null, 2),
}).catch((error: unknown) => {
// Caption auto-save is a data-loss path; surface failures via telemetry
diff --git a/packages/studio/src/hooks/gsapScriptCommitHelpers.ts b/packages/studio/src/hooks/gsapScriptCommitHelpers.ts
index 8c6e45a020..4bca7d0e2c 100644
--- a/packages/studio/src/hooks/gsapScriptCommitHelpers.ts
+++ b/packages/studio/src/hooks/gsapScriptCommitHelpers.ts
@@ -3,6 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
export { PROPERTY_DEFAULTS } from "./gsapShared";
import { idSelector, matchesExactlyOne } from "./gsapShared";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
/**
* The selector to author a NEW tween against, minting an id on the element when
@@ -119,7 +120,7 @@ export async function assignGsapTargetAutoIdIfNeeded({
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify(patchBody),
},
);
diff --git a/packages/studio/src/hooks/timelineTimingSync.ts b/packages/studio/src/hooks/timelineTimingSync.ts
index 919d259a90..aac58e926f 100644
--- a/packages/studio/src/hooks/timelineTimingSync.ts
+++ b/packages/studio/src/hooks/timelineTimingSync.ts
@@ -7,6 +7,7 @@ import { applySoftReload, applySoftReloadFinalization } from "../utils/gsapSoftR
import { furthestClipEndFromDocument } from "../player/lib/timelineElementHelpers";
import type { RecordEditInput } from "../utils/studioFileHistory";
import { patchDocumentRootDuration } from "./timelineEditingGsap";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
class GsapPreviewConvergenceError extends Error {}
class GsapOwnershipProtocolError extends GsapPreviewConvergenceError {}
@@ -58,6 +59,8 @@ async function rollbackOwnedMutation(
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-rollback/${encodeURIComponent(targetPath)}`,
{
method: "POST",
+ // Deliberately unclaimed: a rollback runs because a mutation did not
+ // converge, so let the restored file reload the preview.
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ expected, restore }),
},
@@ -156,7 +159,7 @@ async function postGsapMutation(
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(filePath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify(mutation),
},
);
diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts
index 7a02be735d..deb5ae9e3c 100644
--- a/packages/studio/src/hooks/useDomEditCommits.ts
+++ b/packages/studio/src/hooks/useDomEditCommits.ts
@@ -33,6 +33,7 @@ import {
readErrorResponseBody,
} from "./useDomEditCommitsHelpers";
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
interface RecordEditInput {
label: string;
kind: EditHistoryKind;
@@ -201,7 +202,7 @@ export function useDomEditCommits({
`/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify(patchBody),
},
);
diff --git a/packages/studio/src/hooks/useDomEditCommitsHelpers.ts b/packages/studio/src/hooks/useDomEditCommitsHelpers.ts
index dd82aff581..a440c6e4ef 100644
--- a/packages/studio/src/hooks/useDomEditCommitsHelpers.ts
+++ b/packages/studio/src/hooks/useDomEditCommitsHelpers.ts
@@ -1,6 +1,7 @@
import { StudioSaveHttpError, trackStudioSaveFailure } from "../utils/studioSaveDiagnostics";
import type { DomEditPatchBatch } from "./domEditCommitTypes";
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
return fields.map((field) => field.path).join(", ");
@@ -99,7 +100,7 @@ export async function patchElementBatches(projectId: string, batches: DomEditPat
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body,
},
);
diff --git a/packages/studio/src/hooks/useElementLifecycleOps.ts b/packages/studio/src/hooks/useElementLifecycleOps.ts
index 4e17c5af36..3df6bf402e 100644
--- a/packages/studio/src/hooks/useElementLifecycleOps.ts
+++ b/packages/studio/src/hooks/useElementLifecycleOps.ts
@@ -21,6 +21,7 @@ import {
} from "../components/editor/useLayerRevealOverride";
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
/** Route delete through SDK when session resolves the hf-id. */
@@ -115,7 +116,7 @@ export function useElementLifecycleOps({
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify({ target: patchTarget }),
},
);
diff --git a/packages/studio/src/hooks/useExternalFileChangeCoordinator.ts b/packages/studio/src/hooks/useExternalFileChangeCoordinator.ts
index 4d16842859..b1de697b09 100644
--- a/packages/studio/src/hooks/useExternalFileChangeCoordinator.ts
+++ b/packages/studio/src/hooks/useExternalFileChangeCoordinator.ts
@@ -4,6 +4,7 @@ import { StudioFileConflictError } from "../utils/studioSaveDiagnostics";
import type { ExternalConflictSnapshot } from "../utils/externalConflictStorage";
import { isSelfWriteEcho } from "./sdkSelfWriteRegistry";
import { consumeStudioWriteToken } from "../utils/studioFileVersion";
+import { logReload } from "../utils/reloadDebug";
type ExternalChangeDrainResult =
| { status: "clean" }
@@ -194,6 +195,7 @@ export function useExternalFileChangeCoordinator({
const reloadAcceptedGeneration = useCallback(
(path: string) => {
+ logReload("reload", { path, by: "external-change coordinator" });
reloadPreview();
reloadSdkSession(path);
},
@@ -221,11 +223,22 @@ export function useExternalFileChangeCoordinator({
pendingTimelinePaths.delete(path);
const content = readFileChangeContent(payload);
- if (consumeStudioWriteToken(readFileChangeWriteToken(payload))) return;
- if (content != null && isSelfWriteEcho(path, content)) return;
+ const token = readFileChangeWriteToken(payload);
+ logReload("file-change", { path, token: token ?? null, hasContent: content != null });
+ if (consumeStudioWriteToken(token)) {
+ logReload("suppressed", { path, why: "own write token" });
+ return;
+ }
+ if (content != null && isSelfWriteEcho(path, content)) {
+ logReload("suppressed", { path, why: "own content echo" });
+ return;
+ }
const identity = eventIdentity(path, payload);
- if (!allowDuplicate && identity != null && identity === lastEventIdentityRef.current) return;
+ if (!allowDuplicate && identity != null && identity === lastEventIdentityRef.current) {
+ logReload("suppressed", { path, why: "duplicate event" });
+ return;
+ }
lastEventIdentityRef.current = identity;
const generation = ++generationRef.current;
const result = await drainPendingChanges();
diff --git a/packages/studio/src/hooks/useGroupCommits.ts b/packages/studio/src/hooks/useGroupCommits.ts
index 6e838f1dbe..1ac24bd14e 100644
--- a/packages/studio/src/hooks/useGroupCommits.ts
+++ b/packages/studio/src/hooks/useGroupCommits.ts
@@ -5,6 +5,7 @@ import {
type DomEditCommitBaseParams,
} from "../utils/studioFileHistory";
import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
interface UseGroupCommitsParams extends DomEditCommitBaseParams {
/** Resync the SDK session after a server-side write (the wrapper/unwrap changes
@@ -75,7 +76,7 @@ async function commitStructuralMutation(
`/api/projects/${pid}/file-mutations/${route}/${encodeURIComponent(targetPath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify(body),
},
);
diff --git a/packages/studio/src/hooks/useGsapScriptCommits.ts b/packages/studio/src/hooks/useGsapScriptCommits.ts
index b4b7d6f636..f1a21a3afe 100644
--- a/packages/studio/src/hooks/useGsapScriptCommits.ts
+++ b/packages/studio/src/hooks/useGsapScriptCommits.ts
@@ -36,6 +36,7 @@ import {
useGsapSaveFailureTelemetry,
useSafeGsapCommitMutation,
} from "./useSafeGsapCommitMutation";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
async function mutateGsapScript(
projectId: string,
@@ -46,7 +47,7 @@ async function mutateGsapScript(
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify(mutation),
},
);
@@ -65,7 +66,7 @@ async function mutateGsapScriptBatch(
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify({ mutations }),
},
);
diff --git a/packages/studio/src/hooks/useTimelineEditing.ts b/packages/studio/src/hooks/useTimelineEditing.ts
index bf846f06fe..10f0da7dee 100644
--- a/packages/studio/src/hooks/useTimelineEditing.ts
+++ b/packages/studio/src/hooks/useTimelineEditing.ts
@@ -36,6 +36,7 @@ import { serializeZLaneGesture } from "../components/nle/zLaneGesture";
import { cutoverCommittedOrThrow, sdkTimingPersist } from "../utils/sdkCutover";
import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
+import { studioWriteHeaders } from "../utils/studioFileVersion";
type TimelineMoveUpdates = Pick & {
stackingReorder?: TimelineStackingReorderIntent | null;
@@ -412,7 +413,7 @@ export function useTimelineEditing({
`/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify({ target: patchTarget }),
},
);
diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts
index 4ac8e6be0e..11fc23824f 100644
--- a/packages/studio/src/player/hooks/useTimelinePlayer.ts
+++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts
@@ -4,6 +4,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
import { usePlaybackKeyboard } from "./usePlaybackKeyboard";
import { useTimelineSyncCallbacks } from "./useTimelineSyncCallbacks";
import { useTimelinePlayerLoop } from "./useTimelinePlayerLoop";
+import { logReload } from "../../utils/reloadDebug";
export type { ClipManifestClip } from "../lib/playbackTypes";
export { createStaticSeekPlaybackAdapter } from "../lib/playbackAdapter";
@@ -445,6 +446,7 @@ export function useTimelinePlayer() {
const refreshPlayer = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe) return;
+ logReload("refreshPlayer", { stack: new Error("refreshPlayer").stack });
saveSeekPosition();
// Hide the iframe across the full reload so the user never sees the reloading
// document's RAW DOM (every clip stacked and visible) in the window between the
diff --git a/packages/studio/src/utils/reloadDebug.ts b/packages/studio/src/utils/reloadDebug.ts
new file mode 100644
index 0000000000..40d03c068d
--- /dev/null
+++ b/packages/studio/src/utils/reloadDebug.ts
@@ -0,0 +1,26 @@
+// Preview full-reload diagnostics — grep [hf-reload]. Off by default; opt in per
+// session with `localStorage.setItem("hf-reload-debug", "1")` (then reload).
+//
+// A full reload blanks the stage for ~100-300ms, so any reload the user did not
+// ask for reads as a flash. These lines answer the only question that matters
+// when one appears: who asked for it, and why the write that triggered it was
+// not recognised as Studio's own.
+let enabled: boolean | null = null;
+
+function isEnabled(): boolean {
+ if (enabled === null) {
+ try {
+ enabled = localStorage.getItem("hf-reload-debug") === "1";
+ } catch {
+ enabled = false;
+ }
+ }
+ return enabled;
+}
+
+export function logReload(stage: string, data: Record = {}): void {
+ if (!isEnabled()) return;
+ console.log(
+ `[hf-reload] ${JSON.stringify({ stage, t: Math.round(performance.now()), ...data })}`,
+ );
+}
diff --git a/packages/studio/src/utils/studioFileVersion.ts b/packages/studio/src/utils/studioFileVersion.ts
index 2737c069b0..db89a42166 100644
--- a/packages/studio/src/utils/studioFileVersion.ts
+++ b/packages/studio/src/utils/studioFileVersion.ts
@@ -51,3 +51,18 @@ export async function studioExpectedFileVersion(
export function createStudioWriteToken(): string {
return globalThis.crypto.randomUUID();
}
+
+/**
+ * Headers that claim the write a mutation request is about to make as our own.
+ *
+ * The token is marked BEFORE the request goes out on purpose: the server writes
+ * the file and the watcher broadcasts it while the request is still in flight, so
+ * a token marked from the response can arrive after the echo it was meant to
+ * match. An unmatched echo reads as an external change and costs a full preview
+ * reload, which the user sees as a flash right after their own edit.
+ */
+export function studioWriteHeaders(): Record {
+ const token = createStudioWriteToken();
+ markStudioWriteToken(token);
+ return { "X-Hyperframes-Write-Token": token };
+}
diff --git a/packages/studio/src/utils/timelineCompositionInsert.ts b/packages/studio/src/utils/timelineCompositionInsert.ts
index 144b6c4efb..275905a6d9 100644
--- a/packages/studio/src/utils/timelineCompositionInsert.ts
+++ b/packages/studio/src/utils/timelineCompositionInsert.ts
@@ -2,6 +2,7 @@ import { createStudioSaveHttpError } from "./studioSaveDiagnostics";
import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
import type { RecordEditInput } from "./studioFileHistory";
import { buildProjectApiPath } from "./projectRouting";
+import { studioWriteHeaders } from "./studioFileVersion";
interface TimelineCompositionInsertionResult {
path: string;
@@ -34,7 +35,7 @@ async function insertTimelineComposition(input: {
),
{
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
body: JSON.stringify({
sourcePath: input.sourcePath,
start: input.start,