@hyperframes/studio 0.8.34 → 0.8.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/assets/{hyperframes-player-D4aSRr_M.js → hyperframes-player-BIpJmC69.js} +1 -1
  2. package/dist/assets/index-BmPeyP0k.js +441 -0
  3. package/dist/assets/{index-aNyQBoJv.js → index-Df3VgdgL.js} +1 -1
  4. package/dist/assets/{index-hch4ktcC.js → index-z2_QKypm.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +76 -209
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/App.tsx +0 -7
  10. package/src/components/StudioRightPanel.tsx +0 -7
  11. package/src/components/StudioRightPanel.types.ts +0 -2
  12. package/src/components/editor/SnapToolbar.test.tsx +0 -2
  13. package/src/components/editor/manualEdits.test.ts +3 -3
  14. package/src/components/editor/manualEdits.ts +1 -1
  15. package/src/components/editor/manualEditsParsing.ts +12 -21
  16. package/src/components/panels/VariablesOtherCompositions.tsx +1 -4
  17. package/src/components/panels/VariablesPanel.tsx +1 -5
  18. package/src/components/studioColorGradingScope.ts +0 -4
  19. package/src/hooks/elementLifecycleOpsTestUtils.ts +0 -1
  20. package/src/hooks/externalFileChangeOwnership.test.ts +40 -1
  21. package/src/hooks/gsapScriptCommitTypes.ts +0 -1
  22. package/src/hooks/sdkSelfWriteRegistry.ts +1 -1
  23. package/src/hooks/timelineAudioGroupCreate.ts +0 -7
  24. package/src/hooks/timelineAudioGroupVolume.test.ts +0 -1
  25. package/src/hooks/timelineAudioGroupVolume.ts +0 -6
  26. package/src/hooks/timelineEditingHelpers.test.ts +0 -3
  27. package/src/hooks/timelineEditingHelpers.ts +0 -10
  28. package/src/hooks/timelineElementFxAttribute.ts +0 -6
  29. package/src/hooks/timelineTrackVisibility.test.ts +0 -17
  30. package/src/hooks/timelineTrackVisibility.ts +0 -15
  31. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -1
  32. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -2
  33. package/src/hooks/useAppHotkeys.ts +1 -7
  34. package/src/hooks/useAudioGroupCarveAssignment.test.tsx +0 -1
  35. package/src/hooks/useClipboard.ts +1 -11
  36. package/src/hooks/useDomEditCommits.test.tsx +0 -2
  37. package/src/hooks/useDomEditCommits.ts +2 -21
  38. package/src/hooks/useDomEditSession.test.tsx +0 -1
  39. package/src/hooks/useDomEditSession.ts +0 -7
  40. package/src/hooks/useEditorSave.test.tsx +0 -1
  41. package/src/hooks/useEditorSave.ts +2 -6
  42. package/src/hooks/useElementLifecycleOps.ts +0 -3
  43. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +54 -0
  44. package/src/hooks/useExternalFileChangeCoordinator.ts +38 -22
  45. package/src/hooks/useFileManager.projectOwnership.test.tsx +0 -1
  46. package/src/hooks/useFileManager.ts +0 -3
  47. package/src/hooks/useGroupCommits.ts +0 -2
  48. package/src/hooks/useGsapScriptCommits.test.tsx +0 -2
  49. package/src/hooks/useGsapScriptCommits.ts +2 -5
  50. package/src/hooks/usePreviewInteraction.test.ts +14 -5
  51. package/src/hooks/usePreviewInteraction.ts +3 -3
  52. package/src/hooks/useProjectCompositionVariables.ts +3 -6
  53. package/src/hooks/useRazorSplit.history.test.tsx +0 -2
  54. package/src/hooks/useRazorSplit.test.ts +0 -2
  55. package/src/hooks/useRazorSplit.test.tsx +0 -1
  56. package/src/hooks/useRazorSplit.ts +0 -6
  57. package/src/hooks/useSdkSession.ts +3 -11
  58. package/src/hooks/useSlideshowPersist.ts +1 -5
  59. package/src/hooks/useTimelineAssetDropOps.ts +0 -6
  60. package/src/hooks/useTimelineDeleteOps.ts +0 -4
  61. package/src/hooks/useTimelineEditing.test.tsx +0 -3
  62. package/src/hooks/useTimelineEditing.ts +0 -16
  63. package/src/hooks/useTimelineEditingTypes.ts +0 -1
  64. package/src/hooks/useTimelineGroupEditing.ts +0 -6
  65. package/src/hooks/useVariablesPersist.ts +0 -3
  66. package/src/player/components/Player.tsx +18 -5
  67. package/src/player/components/playerModuleRetry.test.ts +41 -0
  68. package/src/player/lib/playbackTypes.ts +7 -0
  69. package/src/player/lib/timelineIframeHelpers.test.ts +30 -0
  70. package/src/player/lib/timelineIframeHelpers.ts +16 -0
  71. package/src/utils/sdkCutover.familyGate.test.ts +0 -1
  72. package/src/utils/sdkCutover.gate.test.ts +0 -1
  73. package/src/utils/sdkCutover.test.ts +0 -27
  74. package/src/utils/sdkEditTransaction.ts +0 -4
  75. package/src/utils/setSlideshowManifest.test.ts +0 -1
  76. package/src/utils/studioFileHistory.ts +0 -1
  77. package/src/webmcp/types.ts +5 -1
  78. package/src/webmcp/useStudioAgentTools.polyfill.test.tsx +219 -0
  79. package/src/webmcp/useStudioAgentTools.test.tsx +81 -67
  80. package/src/webmcp/useStudioAgentTools.ts +49 -28
  81. package/src/webmcp/webmcpTestUtils.ts +56 -0
  82. package/dist/assets/index-CkxPQxLJ.js +0 -441
  83. package/src/hooks/useSdkSession.test.ts +0 -32
@@ -42,7 +42,6 @@ interface SetElementAttributeInput {
42
42
  previewIframe: HTMLIFrameElement | null;
43
43
  writeProjectFile: (path: string, content: string) => Promise<void>;
44
44
  recordEdit: Parameters<typeof persistElementAttribute>[0]["recordEdit"];
45
- domEditSaveTimestampRef: MutableRef<number>;
46
45
  pendingTimelineEditPathRef: MutableRef<Set<string>>;
47
46
  }
48
47
 
@@ -56,7 +55,6 @@ async function setElementAttribute({
56
55
  previewIframe,
57
56
  writeProjectFile,
58
57
  recordEdit,
59
- domEditSaveTimestampRef,
60
58
  pendingTimelineEditPathRef,
61
59
  }: SetElementAttributeInput): Promise<string[]> {
62
60
  const targetPath = element.sourceFile || activeCompPath || "index.html";
@@ -72,7 +70,6 @@ async function setElementAttribute({
72
70
  label,
73
71
  writeProjectFile,
74
72
  recordEdit,
75
- domEditSaveTimestampRef,
76
73
  pendingTimelineEditPathRef,
77
74
  patchLive: (v) => patchLiveElementAttribute(previewIframe, element, attr, v, activeCompPath),
78
75
  });
@@ -84,7 +81,6 @@ export function useSetElementAttribute({
84
81
  showToast,
85
82
  writeProjectFile,
86
83
  recordEdit,
87
- domEditSaveTimestampRef,
88
84
  previewIframeRef,
89
85
  pendingTimelineEditPathRef,
90
86
  isRecordingRef,
@@ -122,7 +118,6 @@ export function useSetElementAttribute({
122
118
  previewIframe: previewIframeRef.current,
123
119
  writeProjectFile,
124
120
  recordEdit,
125
- domEditSaveTimestampRef,
126
121
  pendingTimelineEditPathRef,
127
122
  });
128
123
  } catch (error) {
@@ -136,7 +131,6 @@ export function useSetElementAttribute({
136
131
  previewIframeRef,
137
132
  writeProjectFile,
138
133
  recordEdit,
139
- domEditSaveTimestampRef,
140
134
  pendingTimelineEditPathRef,
141
135
  isRecordingRef,
142
136
  showToast,
@@ -65,7 +65,6 @@ describe("toggleTimelineTrackHidden", () => {
65
65
 
66
66
  const writes = new Map<string, string>();
67
67
  const recordEdit = vi.fn();
68
- const timestampRef = { current: 0 };
69
68
  const pendingRef = { current: new Set<string>() };
70
69
 
71
70
  await toggleTimelineTrackHidden({
@@ -83,7 +82,6 @@ describe("toggleTimelineTrackHidden", () => {
83
82
  writes.set(path, content);
84
83
  },
85
84
  recordEdit,
86
- domEditSaveTimestampRef: timestampRef,
87
85
  pendingTimelineEditPathRef: pendingRef,
88
86
  });
89
87
 
@@ -96,7 +94,6 @@ describe("toggleTimelineTrackHidden", () => {
96
94
  expect(writes.get("index.html")).toContain('id="skip" data-start="0" data-duration="2"');
97
95
  expect(writes.get("scene.html")).toContain('data-hidden=""');
98
96
  expect(pendingRef.current).toEqual(new Set(["index.html", "scene.html"]));
99
- expect(timestampRef.current).toBeGreaterThan(0);
100
97
  expect(recordEdit).toHaveBeenCalledTimes(1);
101
98
  // Display row, not the raw key: track 0 is the first row, so it reads "1",
102
99
  // the same number the track header announces for that row.
@@ -133,7 +130,6 @@ describe("toggleTimelineTrackHidden", () => {
133
130
  writes.set(path, content);
134
131
  },
135
132
  recordEdit: vi.fn(),
136
- domEditSaveTimestampRef: { current: 0 },
137
133
  pendingTimelineEditPathRef: { current: new Set() },
138
134
  });
139
135
 
@@ -164,7 +160,6 @@ describe("toggleTimelineTrackHidden", () => {
164
160
  previewIframe: null,
165
161
  writeProjectFile: async () => {},
166
162
  recordEdit,
167
- domEditSaveTimestampRef: { current: 0 },
168
163
  pendingTimelineEditPathRef: { current: new Set() },
169
164
  });
170
165
 
@@ -191,7 +186,6 @@ describe("toggleTimelineTrackHidden", () => {
191
186
  previewIframe: null,
192
187
  writeProjectFile: async () => {},
193
188
  recordEdit,
194
- domEditSaveTimestampRef: { current: 0 },
195
189
  pendingTimelineEditPathRef: { current: new Set() },
196
190
  });
197
191
 
@@ -215,7 +209,6 @@ describe("toggleTimelineTrackHidden", () => {
215
209
  previewIframe: null,
216
210
  writeProjectFile: async () => {},
217
211
  recordEdit,
218
- domEditSaveTimestampRef: { current: 0 },
219
212
  pendingTimelineEditPathRef: { current: new Set() },
220
213
  });
221
214
 
@@ -246,7 +239,6 @@ describe("toggleTimelineTrackHidden", () => {
246
239
  previewIframe: null,
247
240
  writeProjectFile: async () => {},
248
241
  recordEdit,
249
- domEditSaveTimestampRef: { current: 0 },
250
242
  pendingTimelineEditPathRef: { current: new Set() },
251
243
  });
252
244
 
@@ -272,7 +264,6 @@ describe("toggleTimelineTrackHidden", () => {
272
264
  previewIframe: null,
273
265
  writeProjectFile: async () => {},
274
266
  recordEdit,
275
- domEditSaveTimestampRef: { current: 0 },
276
267
  pendingTimelineEditPathRef: { current: new Set() },
277
268
  });
278
269
 
@@ -303,7 +294,6 @@ describe("toggleTimelineTrackHidden", () => {
303
294
  previewIframe: null,
304
295
  writeProjectFile: async () => {},
305
296
  recordEdit,
306
- domEditSaveTimestampRef: { current: 0 },
307
297
  pendingTimelineEditPathRef: { current: new Set() },
308
298
  });
309
299
 
@@ -359,7 +349,6 @@ describe("toggleTimelineElementHidden", () => {
359
349
  writes.set(path, content);
360
350
  },
361
351
  recordEdit,
362
- domEditSaveTimestampRef: { current: 0 },
363
352
  pendingTimelineEditPathRef: { current: new Set() },
364
353
  });
365
354
 
@@ -417,7 +406,6 @@ describe("toggleTimelineElementHidden", () => {
417
406
  writes.push({ path, content });
418
407
  },
419
408
  recordEdit,
420
- domEditSaveTimestampRef: { current: 0 },
421
409
  pendingTimelineEditPathRef: { current: new Set() },
422
410
  });
423
411
 
@@ -484,7 +472,6 @@ describe("createAudioGroupAndAssignMembers", () => {
484
472
  writes.set(path, content);
485
473
  },
486
474
  recordEdit,
487
- domEditSaveTimestampRef: { current: 0 },
488
475
  pendingTimelineEditPathRef: { current: new Set() },
489
476
  });
490
477
 
@@ -557,7 +544,6 @@ describe("createAudioGroupAndAssignMembers", () => {
557
544
  writes.set(path, content);
558
545
  },
559
546
  recordEdit: vi.fn(),
560
- domEditSaveTimestampRef: { current: 0 },
561
547
  pendingTimelineEditPathRef: { current: new Set() },
562
548
  });
563
549
 
@@ -592,7 +578,6 @@ describe("createAudioGroupAndAssignMembers", () => {
592
578
  previewIframe: null,
593
579
  writeProjectFile: async () => {},
594
580
  recordEdit,
595
- domEditSaveTimestampRef: { current: 0 },
596
581
  pendingTimelineEditPathRef: { current: new Set() },
597
582
  }),
598
583
  ).rejects.toThrow("a group needs at least two");
@@ -612,7 +597,6 @@ describe("createAudioGroupAndAssignMembers", () => {
612
597
  previewIframe: null,
613
598
  writeProjectFile: async () => {},
614
599
  recordEdit: vi.fn(),
615
- domEditSaveTimestampRef: { current: 0 },
616
600
  pendingTimelineEditPathRef: { current: new Set() },
617
601
  }),
618
602
  ).rejects.toThrow("Invalid audio group id");
@@ -641,7 +625,6 @@ describe("createAudioGroupAndAssignMembers", () => {
641
625
  previewIframe: iframe,
642
626
  writeProjectFile: async () => {},
643
627
  recordEdit: vi.fn(),
644
- domEditSaveTimestampRef: { current: 0 },
645
628
  pendingTimelineEditPathRef: { current: new Set() },
646
629
  }),
647
630
  ).rejects.toThrow();
@@ -39,7 +39,6 @@ interface ToggleTimelineTrackHiddenInput {
39
39
  previewIframe: HTMLIFrameElement | null;
40
40
  writeProjectFile: (path: string, content: string) => Promise<void>;
41
41
  recordEdit: (input: RecordEditInput) => Promise<void>;
42
- domEditSaveTimestampRef: MutableRef<number>;
43
42
  pendingTimelineEditPathRef: MutableRef<Set<string>>;
44
43
  }
45
44
 
@@ -57,7 +56,6 @@ interface SetElementsHiddenInput {
57
56
  previewIframe: HTMLIFrameElement | null;
58
57
  writeProjectFile: (path: string, content: string) => Promise<void>;
59
58
  recordEdit: (input: RecordEditInput) => Promise<void>;
60
- domEditSaveTimestampRef: MutableRef<number>;
61
59
  pendingTimelineEditPathRef: MutableRef<Set<string>>;
62
60
  }
63
61
 
@@ -140,7 +138,6 @@ async function setElementsHidden({
140
138
  previewIframe,
141
139
  writeProjectFile,
142
140
  recordEdit,
143
- domEditSaveTimestampRef,
144
141
  pendingTimelineEditPathRef,
145
142
  }: SetElementsHiddenInput): Promise<string[]> {
146
143
  if (elements.length === 0) return [];
@@ -176,7 +173,6 @@ async function setElementsHidden({
176
173
  pendingTimelineEditPathRef.current.add(targetPath);
177
174
  }
178
175
 
179
- domEditSaveTimestampRef.current = Date.now();
180
176
  const changedPaths = await saveProjectFilesWithHistory({
181
177
  projectId,
182
178
  label,
@@ -190,7 +186,6 @@ async function setElementsHidden({
190
186
  writeFile: writeProjectFile,
191
187
  recordEdit,
192
188
  });
193
- domEditSaveTimestampRef.current = Date.now();
194
189
  for (const element of elements) {
195
190
  usePlayerStore.getState().updateElement(element.key ?? element.id, { hidden });
196
191
  }
@@ -215,7 +210,6 @@ export async function toggleTimelineTrackHidden({
215
210
  previewIframe,
216
211
  writeProjectFile,
217
212
  recordEdit,
218
- domEditSaveTimestampRef,
219
213
  pendingTimelineEditPathRef,
220
214
  }: ToggleTimelineTrackHiddenInput): Promise<string[]> {
221
215
  // `track` is the fractional sort key the callback needs; the history entry is
@@ -245,7 +239,6 @@ export async function toggleTimelineTrackHidden({
245
239
  previewIframe,
246
240
  writeProjectFile,
247
241
  recordEdit,
248
- domEditSaveTimestampRef,
249
242
  pendingTimelineEditPathRef,
250
243
  });
251
244
  }
@@ -259,7 +252,6 @@ export async function toggleTimelineElementHidden({
259
252
  previewIframe,
260
253
  writeProjectFile,
261
254
  recordEdit,
262
- domEditSaveTimestampRef,
263
255
  pendingTimelineEditPathRef,
264
256
  }: ToggleTimelineElementHiddenInput): Promise<string[]> {
265
257
  const keys = new Set(typeof elementKey === "string" ? [elementKey] : elementKey);
@@ -280,7 +272,6 @@ export async function toggleTimelineElementHidden({
280
272
  previewIframe,
281
273
  writeProjectFile,
282
274
  recordEdit,
283
- domEditSaveTimestampRef,
284
275
  pendingTimelineEditPathRef,
285
276
  });
286
277
  }
@@ -291,7 +282,6 @@ export function useTimelineTrackVisibilityEditing({
291
282
  showToast,
292
283
  writeProjectFile,
293
284
  recordEdit,
294
- domEditSaveTimestampRef,
295
285
  previewIframeRef,
296
286
  pendingTimelineEditPathRef,
297
287
  isRecordingRef,
@@ -325,7 +315,6 @@ export function useTimelineTrackVisibilityEditing({
325
315
  previewIframe: previewIframeRef.current,
326
316
  writeProjectFile,
327
317
  recordEdit,
328
- domEditSaveTimestampRef,
329
318
  pendingTimelineEditPathRef,
330
319
  });
331
320
  forceReloadSdkSession?.();
@@ -342,7 +331,6 @@ export function useTimelineTrackVisibilityEditing({
342
331
  previewIframeRef,
343
332
  writeProjectFile,
344
333
  recordEdit,
345
- domEditSaveTimestampRef,
346
334
  pendingTimelineEditPathRef,
347
335
  isRecordingRef,
348
336
  showToast,
@@ -358,7 +346,6 @@ export function useTimelineElementVisibilityEditing({
358
346
  showToast,
359
347
  writeProjectFile,
360
348
  recordEdit,
361
- domEditSaveTimestampRef,
362
349
  previewIframeRef,
363
350
  pendingTimelineEditPathRef,
364
351
  isRecordingRef,
@@ -394,7 +381,6 @@ export function useTimelineElementVisibilityEditing({
394
381
  previewIframe: previewIframeRef.current,
395
382
  writeProjectFile,
396
383
  recordEdit,
397
- domEditSaveTimestampRef,
398
384
  pendingTimelineEditPathRef,
399
385
  });
400
386
  forceReloadSdkSession?.();
@@ -411,7 +397,6 @@ export function useTimelineElementVisibilityEditing({
411
397
  previewIframeRef,
412
398
  writeProjectFile,
413
399
  recordEdit,
414
- domEditSaveTimestampRef,
415
400
  pendingTimelineEditPathRef,
416
401
  isRecordingRef,
417
402
  showToast,
@@ -43,7 +43,6 @@ function Harness() {
43
43
  readOptionalProjectFile: vi.fn(async () => ""),
44
44
  readProjectFile: vi.fn(async () => ""),
45
45
  writeProjectFile: vi.fn(async () => undefined),
46
- domEditSaveTimestampRef: useRef(0),
47
46
  showToast: vi.fn(),
48
47
  syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
49
48
  waitForPendingDomEditSaves: vi.fn(async () => undefined),
@@ -70,7 +70,6 @@ function Harness() {
70
70
  const [selectionRefreshed, setSelectionRefreshed] = useState(false);
71
71
  const selectionRef = useRef<DomEditSelection | null>(parentWithTextChild());
72
72
  const clearSelectionRef = useRef<() => void>(() => undefined);
73
- const saveTimestampRef = useRef(0);
74
73
  const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
75
74
 
76
75
  useAppHotkeys({
@@ -87,7 +86,6 @@ function Harness() {
87
86
  readOptionalProjectFile: vi.fn(async () => ""),
88
87
  readProjectFile: vi.fn(async () => ""),
89
88
  writeProjectFile: vi.fn(async () => undefined),
90
- domEditSaveTimestampRef: saveTimestampRef,
91
89
  showToast: vi.fn(),
92
90
  syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
93
91
  waitForPendingDomEditSaves: vi.fn(async () => undefined),
@@ -118,7 +118,6 @@ interface UseAppHotkeysParams {
118
118
  readOptionalProjectFile: (path: string) => Promise<string>;
119
119
  readProjectFile: (path: string) => Promise<string>;
120
120
  writeProjectFile: (path: string, content: string) => Promise<void>;
121
- domEditSaveTimestampRef: React.MutableRefObject<number>;
122
121
  showToast: (message: string, tone?: "error" | "info") => void;
123
122
  syncHistoryPreviewAfterApply: (restore: {
124
123
  paths?: string[];
@@ -377,7 +376,6 @@ export function useAppHotkeys({
377
376
  readOptionalProjectFile,
378
377
  readProjectFile,
379
378
  writeProjectFile,
380
- domEditSaveTimestampRef,
381
379
  showToast,
382
380
  syncHistoryPreviewAfterApply,
383
381
  waitForPendingDomEditSaves,
@@ -405,10 +403,9 @@ export function useAppHotkeys({
405
403
  );
406
404
  const writeHistoryFile = useCallback(
407
405
  async (path: string, content: string): Promise<void> => {
408
- domEditSaveTimestampRef.current = Date.now();
409
406
  await writeProjectFile(path, content);
410
407
  },
411
- [domEditSaveTimestampRef, writeProjectFile],
408
+ [writeProjectFile],
412
409
  );
413
410
  const serializeHistoryFiles = useCallback(
414
411
  <T>(paths: readonly string[], task: () => Promise<T>) =>
@@ -455,9 +452,6 @@ export function useAppHotkeys({
455
452
  onAfterUndoRedo?.();
456
453
  // If the active composition was among the written files, force-reload
457
454
  // the SDK session so its in-memory doc matches the reverted content.
458
- // writeHistoryFile sets domEditSaveTimestampRef which activates the
459
- // 2 s suppress window — without this call the file-change event would
460
- // be swallowed and the SDK session would stay on stale pre-undo content.
461
455
  if (activeCompPath && result.paths?.includes(activeCompPath)) {
462
456
  forceReloadSdkSession?.();
463
457
  }
@@ -72,7 +72,6 @@ function renderAssign(writeProjectFile: (path: string, content: string) => Promi
72
72
  showToast,
73
73
  writeProjectFile,
74
74
  recordEdit: async () => {},
75
- domEditSaveTimestampRef: { current: 0 },
76
75
  pendingTimelineEditPathRef: { current: new Set() },
77
76
  previewIframeRef: { current: null },
78
77
  });
@@ -25,7 +25,6 @@ interface UseClipboardOptions {
25
25
  showToast: (message: string, tone?: "error" | "info") => void;
26
26
  writeProjectFile: (path: string, content: string) => Promise<void>;
27
27
  recordEdit: (input: RecordEditInput) => Promise<void>;
28
- domEditSaveTimestampRef: React.MutableRefObject<number>;
29
28
  reloadPreview: () => void;
30
29
  handleTimelineElementDelete: (element: TimelineElement) => Promise<void>;
31
30
  handleDomEditElementDelete: (selection: DomEditSelection) => Promise<void>;
@@ -55,7 +54,6 @@ export function useClipboard({
55
54
  showToast,
56
55
  writeProjectFile,
57
56
  recordEdit,
58
- domEditSaveTimestampRef,
59
57
  reloadPreview,
60
58
  handleTimelineElementDelete,
61
59
  handleDomEditElementDelete,
@@ -171,7 +169,6 @@ export function useClipboard({
171
169
  );
172
170
  }
173
171
 
174
- domEditSaveTimestampRef.current = Date.now();
175
172
  await saveProjectFilesWithHistory({
176
173
  projectId: pid,
177
174
  label: payload.kind === "timeline-clip" ? "Paste clip" : "Paste element",
@@ -188,14 +185,7 @@ export function useClipboard({
188
185
  const message = error instanceof Error ? error.message : "Failed to paste";
189
186
  showToast(message);
190
187
  }
191
- }, [
192
- activeCompPath,
193
- domEditSaveTimestampRef,
194
- recordEdit,
195
- reloadPreview,
196
- showToast,
197
- writeProjectFile,
198
- ]);
188
+ }, [activeCompPath, recordEdit, reloadPreview, showToast, writeProjectFile]);
199
189
 
200
190
  const handleCut = useCallback(async (): Promise<boolean> => {
201
191
  const copied = handleCopy();
@@ -209,7 +209,6 @@ function renderDomEditCommits(
209
209
  const recordEdit = vi.fn(async () => {});
210
210
  const previewIframeRef: MutableRefObject<HTMLIFrameElement | null> = { current: iframe };
211
211
  const projectIdRef: MutableRefObject<string | null> = options.projectIdRef ?? { current: "p1" };
212
- const domEditSaveTimestampRef: MutableRefObject<number> = { current: 0 };
213
212
  const reloadPreview = vi.fn();
214
213
 
215
214
  function Probe() {
@@ -219,7 +218,6 @@ function renderDomEditCommits(
219
218
  showToast,
220
219
  queueDomEditSave: options.queueDomEditSave ?? (async (save) => save()),
221
220
  writeProjectFile: options.writeProjectFile ?? (async () => {}),
222
- domEditSaveTimestampRef,
223
221
  editHistory: { recordEdit },
224
222
  fileTree: [],
225
223
  importedFontAssetsRef: { current: options.importedFontAssets ?? [] },
@@ -50,7 +50,6 @@ export interface UseDomEditCommitsParams {
50
50
  showToast: (message: string, tone?: "error" | "info") => void;
51
51
  queueDomEditSave: <T>(save: () => Promise<T>) => Promise<T>;
52
52
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
53
- domEditSaveTimestampRef: React.MutableRefObject<number>;
54
53
  editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
55
54
  fileTree: string[];
56
55
  importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
@@ -98,7 +97,6 @@ export function useDomEditCommits({
98
97
  showToast,
99
98
  queueDomEditSave,
100
99
  writeProjectFile,
101
- domEditSaveTimestampRef,
102
100
  editHistory,
103
101
  fileTree,
104
102
  importedFontAssetsRef,
@@ -214,11 +212,6 @@ export function useDomEditCommits({
214
212
  }
215
213
  }
216
214
 
217
- // Mark the save timestamp before the file write so the SSE file-change
218
- // handler suppresses the reload even if the event arrives before the
219
- // response (the server writes the file and emits SSE during the fetch).
220
- domEditSaveTimestampRef.current = Date.now();
221
-
222
215
  const patchResponse = await fetch(
223
216
  buildProjectApiPath(pid, `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`),
224
217
  {
@@ -315,7 +308,6 @@ export function useDomEditCommits({
315
308
  editHistory,
316
309
  writeProjectFile,
317
310
  projectIdRef,
318
- domEditSaveTimestampRef,
319
311
  reloadPreview,
320
312
  showToast,
321
313
  forceReloadSdkSession,
@@ -359,7 +351,6 @@ export function useDomEditCommits({
359
351
  );
360
352
  }
361
353
 
362
- domEditSaveTimestampRef.current = Date.now();
363
354
  const atomicResult = await patchElementBatches(pid, batches);
364
355
  const allMatched =
365
356
  atomicResult.durable && atomicResult.files.every((result) => result.allMatched);
@@ -385,8 +376,7 @@ export function useDomEditCommits({
385
376
  // produces a visible blink. Skip the reload when the caller asked for it
386
377
  // AND the persist is provably in sync: style-only ops, every target
387
378
  // matched. Any unmatched patch means the live DOM now shows state disk
388
- // doesn't hold — reload so the preview reconverges. (The SSE/file-watcher
389
- // reload is independently suppressed by domEditSaveTimestampRef above.)
379
+ // doesn't hold — reload so the preview reconverges.
390
380
  const skipSafe =
391
381
  options.skipReload === true && batchesAreInlineStyleOnly(batches) && durable;
392
382
  if (!durable || (changed && !skipSafe)) reloadPreview();
@@ -411,15 +401,7 @@ export function useDomEditCommits({
411
401
  throw error;
412
402
  });
413
403
  },
414
- [
415
- domEditSaveTimestampRef,
416
- editHistory,
417
- forceReloadSdkSession,
418
- projectIdRef,
419
- queueDomEditSave,
420
- reloadPreview,
421
- showToast,
422
- ],
404
+ [editHistory, forceReloadSdkSession, projectIdRef, queueDomEditSave, reloadPreview, showToast],
423
405
  );
424
406
 
425
407
  // ── Text & style commits (delegated to useDomEditTextCommits) ──
@@ -479,7 +461,6 @@ export function useDomEditCommits({
479
461
  activeCompPath,
480
462
  showToast,
481
463
  writeProjectFile,
482
- domEditSaveTimestampRef,
483
464
  editHistory,
484
465
  projectIdRef,
485
466
  reloadPreview,
@@ -84,7 +84,6 @@ function createSessionParams(
84
84
  readProjectFile: async () => "",
85
85
  writeProjectFile: async () => {},
86
86
  updateEditingFileContent: vi.fn(),
87
- domEditSaveTimestampRef: { current: 0 },
88
87
  editHistory: { recordEdit: async () => {} },
89
88
  fileTree: [],
90
89
  importedFontAssetsRef: { current: [] },
@@ -47,7 +47,6 @@ export interface UseDomEditSessionParams {
47
47
  readProjectFile: (path: string) => Promise<string>;
48
48
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
49
49
  updateEditingFileContent: (path: string, content: string) => void;
50
- domEditSaveTimestampRef: React.MutableRefObject<number>;
51
50
  editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
52
51
  fileTree: string[];
53
52
  importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
@@ -91,7 +90,6 @@ export function useDomEditSession({
91
90
  readProjectFile,
92
91
  writeProjectFile,
93
92
  updateEditingFileContent,
94
- domEditSaveTimestampRef,
95
93
  editHistory,
96
94
  fileTree,
97
95
  importedFontAssetsRef,
@@ -211,7 +209,6 @@ export function useDomEditSession({
211
209
  activeCompPath,
212
210
  previewIframeRef,
213
211
  editHistory,
214
- domEditSaveTimestampRef,
215
212
  reloadPreview,
216
213
  onCacheInvalidate: bumpGsapCache,
217
214
  onFileContentChanged: updateEditingFileContent,
@@ -249,7 +246,6 @@ export function useDomEditSession({
249
246
  showToast,
250
247
  queueDomEditSave,
251
248
  writeProjectFile,
252
- domEditSaveTimestampRef,
253
249
  editHistory,
254
250
  fileTree,
255
251
  importedFontAssetsRef,
@@ -283,7 +279,6 @@ export function useDomEditSession({
283
279
  editHistory,
284
280
  writeProjectFile,
285
281
  reloadPreview,
286
- domEditSaveTimestampRef,
287
282
  compositionPath: activeCompPath,
288
283
  readProjectFile,
289
284
  publishSession: publishSdkSession,
@@ -298,7 +293,6 @@ export function useDomEditSession({
298
293
  editHistory,
299
294
  writeProjectFile,
300
295
  reloadPreview,
301
- domEditSaveTimestampRef,
302
296
  compositionPath: activeCompPath,
303
297
  readProjectFile,
304
298
  publishSession: publishSdkSession,
@@ -327,7 +321,6 @@ export function useDomEditSession({
327
321
  activeCompPath,
328
322
  showToast,
329
323
  writeProjectFile,
330
- domEditSaveTimestampRef,
331
324
  editHistory,
332
325
  projectIdRef,
333
326
  reloadPreview,
@@ -27,7 +27,6 @@ async function mountEditorSave(writeProjectFile: WriteProjectFile) {
27
27
  readProjectFile: vi.fn(async () => "before"),
28
28
  writeProjectFile,
29
29
  recordEdit: vi.fn(async () => undefined),
30
- domEditSaveTimestampRef: { current: 0 },
31
30
  setRefreshKey: vi.fn(),
32
31
  showToast,
33
32
  });
@@ -23,7 +23,6 @@ interface UseEditorSaveOptions {
23
23
  readProjectFile: (path: string) => Promise<string>;
24
24
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
25
25
  recordEdit: (input: RecordEditInput) => Promise<void>;
26
- domEditSaveTimestampRef: React.MutableRefObject<number>;
27
26
  setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
28
27
  showToast: (message: string, tone?: "error" | "info") => void;
29
28
  }
@@ -53,7 +52,6 @@ export function useEditorSave({
53
52
  readProjectFile,
54
53
  writeProjectFile,
55
54
  recordEdit,
56
- domEditSaveTimestampRef,
57
55
  setRefreshKey,
58
56
  showToast,
59
57
  }: UseEditorSaveOptions): EditorSaveHandle {
@@ -156,11 +154,10 @@ export function useEditorSave({
156
154
  if (saveRafRef.current != null) cancelAnimationFrame(saveRafRef.current);
157
155
  saveRafRef.current = requestAnimationFrame(() => {
158
156
  saveRafRef.current = null;
159
- domEditSaveTimestampRef.current = Date.now();
160
157
  void persistCandidate(candidate);
161
158
  });
162
159
  },
163
- [domEditSaveTimestampRef, editingPathRef, projectIdRef, persistCandidate],
160
+ [editingPathRef, projectIdRef, persistCandidate],
164
161
  );
165
162
 
166
163
  const flushPendingSave = useCallback(async (): Promise<EditorSaveDrainResult> => {
@@ -173,11 +170,10 @@ export function useEditorSave({
173
170
  return inFlightRef.current;
174
171
  }
175
172
  if (candidate) {
176
- domEditSaveTimestampRef.current = Date.now();
177
173
  return persistCandidate(candidate);
178
174
  }
179
175
  return (await inFlightRef.current) ?? { status: "clean" };
180
- }, [domEditSaveTimestampRef, persistCandidate]);
176
+ }, [persistCandidate]);
181
177
 
182
178
  const discardPendingSave = useCallback(() => {
183
179
  if (saveRafRef.current != null) cancelAnimationFrame(saveRafRef.current);
@@ -75,7 +75,6 @@ export function useElementLifecycleOps({
75
75
  activeCompPath,
76
76
  showToast,
77
77
  writeProjectFile,
78
- domEditSaveTimestampRef,
79
78
  editHistory,
80
79
  projectIdRef,
81
80
  reloadPreview,
@@ -147,7 +146,6 @@ export function useElementLifecycleOps({
147
146
  }
148
147
  }
149
148
 
150
- domEditSaveTimestampRef.current = Date.now();
151
149
  // One request for the whole selection. Removing members one at a time
152
150
  // cost a round trip and a rewrite of the file EACH, and a canvas
153
151
  // selection runs to hundreds of members — the file ended up correct, but
@@ -220,7 +218,6 @@ export function useElementLifecycleOps({
220
218
  [
221
219
  activeCompPath,
222
220
  clearDomSelection,
223
- domEditSaveTimestampRef,
224
221
  editHistory.recordEdit,
225
222
  onTrySdkDelete,
226
223
  onElementDeleted,