Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export function StudioApp() {
pendingTimelineEditPathRef,
uploadProjectFiles: fileManager.uploadProjectFiles,
isRecordingRef: isGestureRecordingRef,
sdkSession: sdkHandle.session,
});
const {
activeBlockParams,
Expand Down Expand Up @@ -305,7 +304,6 @@ export function StudioApp() {
openSourceForSelection: fileManager.openSourceForSelection,
selectSidebarTab: selectSidebarTabStable,
getSidebarTab: getSidebarTabStable,
sdkSession: sdkHandle.session,
});
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
clearDomSelectionRef.current = domEditSession.clearDomSelection;
Expand Down
11 changes: 0 additions & 11 deletions packages/studio/src/components/editor/manualEditingAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,6 @@ export const STUDIO_GSAP_DRAG_INTERCEPT_ENABLED = resolveStudioBooleanEnvFlag(

export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;

// Stage 7 Step 3b: shadow dispatch parity mode — dispatches ops to the SDK
// session alongside the server patch path and logs mismatches via telemetry.
// Default on: server stays authoritative (no user-visible change), so we want
// the sdk_shadow_dispatch parity signal from all traffic. Disable via
// VITE_STUDIO_SDK_SHADOW_ENABLED=false.
export const STUDIO_SDK_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_SDK_SHADOW_ENABLED"],
true,
);

// Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
// instead of the server patch-element API. Default false; enable via
// VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open.
Expand Down
9 changes: 0 additions & 9 deletions packages/studio/src/hooks/gsapScriptCommitTypes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ParsedGsap } from "@hyperframes/core/gsap-parser";
import type { Composition } from "@hyperframes/sdk";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import type { EditHistoryKind } from "../utils/editHistory";
import type { ShadowGsapOp } from "../utils/sdkShadow";
import type { ShadowKeyframeOp } from "../utils/sdkShadowGsapKeyframe";

export interface MutationResult {
ok: boolean;
Expand All @@ -28,10 +25,6 @@ export interface CommitMutationOptions {
* (and under distinct keys) run concurrently as before.
*/
serializeKey?: string;
/** Stage 7 Step 3b: typed SDK equivalent of this mutation for value-fidelity shadow. */
shadowGsapOp?: ShadowGsapOp;
/** Typed SDK equivalent of a keyframe mutation for keyframe value-fidelity shadow (gsap_keyframe). */
shadowKeyframeOp?: ShadowKeyframeOp;
}

export type CommitMutation = (
Expand Down Expand Up @@ -70,6 +63,4 @@ export interface GsapScriptCommitsParams {
onCacheInvalidate: () => void;
onFileContentChanged?: (path: string, content: string) => void;
showToast: (message: string, tone?: "error" | "info") => void;
/** Stage 7 Step 3b: SDK session for shadow GSAP dispatch (server stays authoritative). */
sdkSession?: Composition | null;
}
10 changes: 0 additions & 10 deletions packages/studio/src/hooks/useDomEditSession.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Composition } from "@hyperframes/sdk";
import type { TimelineElement } from "../player";
import type { ImportedFontAsset } from "../components/editor/fontAssets";
import type { EditHistoryKind } from "../utils/editHistory";
Expand All @@ -9,7 +8,6 @@ import { useAskAgentModal } from "./useAskAgentModal";
import { useDomSelection } from "./useDomSelection";
import { usePreviewInteraction } from "./usePreviewInteraction";
import { useDomEditCommits } from "./useDomEditCommits";
import { runShadowDispatch, runShadowDelete } from "../utils/sdkShadow";
import { useGsapScriptCommits } from "./useGsapScriptCommits";
import { useGsapCacheVersion } from "./useGsapTweenCache";
import { useDomEditWiring } from "./useDomEditWiring";
Expand Down Expand Up @@ -60,8 +58,6 @@ export interface UseDomEditSessionParams {
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
selectSidebarTab?: (tab: SidebarTab) => void;
getSidebarTab?: () => SidebarTab;
/** Stage 7 Step 3b: SDK session for shadow dispatch parity tracking. */
sdkSession?: Composition | null;
}

// ── Hook ──
Expand Down Expand Up @@ -100,7 +96,6 @@ export function useDomEditSession({
openSourceForSelection,
selectSidebarTab,
getSidebarTab,
sdkSession,
}: UseDomEditSessionParams) {
void _setRefreshKey;
void _readProjectFile;
Expand Down Expand Up @@ -194,7 +189,6 @@ export function useDomEditSession({
onCacheInvalidate: bumpGsapCache,
onFileContentChanged: updateEditingFileContent,
showToast,
sdkSession,
});

// ── DOM commit handlers ──
Expand Down Expand Up @@ -234,10 +228,6 @@ export function useDomEditSession({
clearDomSelection,
refreshDomEditSelectionFromPreview,
buildDomSelectionFromTarget,
onDomEditPersisted: sdkSession
? (sel, ops) => runShadowDispatch(sdkSession, sel, ops)
: undefined,
onElementDeleted: sdkSession ? (sel) => runShadowDelete(sdkSession, sel.hfId) : undefined,
});

// ── Wiring: selection sync, GSAP cache, preview sync, selection handlers ──
Expand Down
51 changes: 6 additions & 45 deletions packages/studio/src/hooks/useGsapAnimationOps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useCallback } from "react";
import type { Composition } from "@hyperframes/sdk";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { roundTo3 } from "../utils/rounding";
import { runShadowGsapTween, type ShadowGsapOp } from "../utils/sdkShadow";
import {
assignGsapTargetAutoIdIfNeeded,
ensureElementAddressable,
Expand All @@ -15,8 +13,6 @@ interface GsapAnimationOpsParams {
commitMutation: CommitMutation;
commitMutationSafely: SafeGsapCommitMutation;
showToast: (message: string, tone?: "error" | "info") => void;
/** Stage 7 Step 3b: SDK session for shadow GSAP dispatch (server stays authoritative). */
sdkSession?: Composition | null;
}

export function useGsapAnimationOps({
Expand All @@ -25,46 +21,35 @@ export function useGsapAnimationOps({
commitMutation,
commitMutationSafely,
showToast,
sdkSession,
}: GsapAnimationOpsParams) {
const updateGsapMeta = useCallback(
(
selection: DomEditSelection,
animationId: string,
updates: { duration?: number; ease?: string; position?: number },
) => {
// Shadow op (server animationId shares the SDK id-space): existence via
// runShadowGsapTween (live session) + value fidelity via the chokepoint.
const shadowGsapOp: ShadowGsapOp = {
kind: "set",
animationId,
properties: { duration: updates.duration, ease: updates.ease, position: updates.position },
};
// coalesceKey groups rapid meta edits into one history entry. Request
// serialization is now handled per-file at the commitMutation chokepoint
// (useGsapScriptCommits), so no per-op serializeKey is needed here.
const metaKey = `gsap:${animationId}:meta`;
commitMutationSafely(
selection,
{ type: "update-meta", animationId, updates },
{ label: "Edit GSAP animation", coalesceKey: metaKey, shadowGsapOp },
{ label: "Edit GSAP animation", coalesceKey: metaKey },
);
if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
},
[commitMutationSafely, sdkSession],
[commitMutationSafely],
);

const deleteGsapAnimation = useCallback(
(selection: DomEditSelection, animationId: string) => {
const shadowGsapOp: ShadowGsapOp = { kind: "remove", animationId };
commitMutationSafely(
selection,
{ type: "delete", animationId, stripStudioEdits: true },
{ label: "Delete GSAP animation", shadowGsapOp },
{ label: "Delete GSAP animation" },
);
if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
},
[commitMutationSafely, sdkSession],
[commitMutationSafely],
);

const deleteAllForSelector = useCallback(
Expand All @@ -78,8 +63,6 @@ export function useGsapAnimationOps({
[commitMutation],
);

// Pre-existing complexity (auto-id assignment + per-method defaults); this PR
// adds only a guarded shadow-op construction at the tail.
const addGsapAnimation = useCallback(
// fallow-ignore-next-line complexity
async (
Expand Down Expand Up @@ -114,26 +97,6 @@ export function useGsapAnimationOps({
fromTo: { x: 0, y: 0, opacity: 1 },
};

// Shadow op (server stays authoritative). "set" has no SDK method, so it
// is not shadowed; otherwise: existence via runShadowGsapTween (live) +
// value fidelity via the chokepoint (shadowGsapOp in options).
const shadowGsapOp: ShadowGsapOp | undefined =
selection.hfId && method !== "set"
? {
kind: "add",
target: selection.hfId,
tween: {
method,
position,
duration,
ease: "power2.out",
...(method === "fromTo"
? { fromProperties: { opacity: 0 }, toProperties: toDefaults[method] }
: { properties: toDefaults[method] ?? { opacity: 1 } }),
},
}
: undefined;

await commitMutation(
selection,
{
Expand All @@ -146,12 +109,10 @@ export function useGsapAnimationOps({
properties: toDefaults[method] ?? { opacity: 1 },
fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
},
{ label: `Add GSAP ${method} animation`, shadowGsapOp },
{ label: `Add GSAP ${method} animation` },
);

if (sdkSession && shadowGsapOp) runShadowGsapTween(sdkSession, shadowGsapOp);
},
[activeCompPath, commitMutation, projectIdRef, showToast, sdkSession],
[activeCompPath, commitMutation, projectIdRef, showToast],
);

return {
Expand Down
22 changes: 1 addition & 21 deletions packages/studio/src/hooks/useGsapKeyframeOps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { ShadowKeyframeOp } from "../utils/sdkShadowGsapKeyframe";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { executeOptimistic } from "../utils/optimisticUpdate";
import type { KeyframeCacheEntry } from "../player/store/playerStore";
Expand Down Expand Up @@ -59,13 +58,6 @@ export function useGsapKeyframeOps({
percentage,
properties: { [property]: value },
};
// Shadow op (gsap_keyframe): SDK equivalent diffed via the commit chokepoint.
const shadowKeyframeOp: ShadowKeyframeOp = {
kind: "add",
animationId,
percentage,
properties: { [property]: value },
};
void executeOptimisticKeyframeCacheUpdate({
sourceFile,
elementId: selection.id,
Expand All @@ -79,7 +71,6 @@ export function useGsapKeyframeOps({
commitMutation(selection, mutation, {
label: `Add keyframe at ${percentage}%`,
softReload: true,
shadowKeyframeOp,
}),
}).catch((error) => {
trackGsapSaveFailure(error, selection, mutation, `Add keyframe at ${percentage}%`);
Expand All @@ -95,16 +86,10 @@ export function useGsapKeyframeOps({
percentage: number,
properties: Record<string, number | string>,
) => {
const shadowKeyframeOp: ShadowKeyframeOp = {
kind: "add",
animationId,
percentage,
properties,
};
return commitMutation(
selection,
{ type: "add-keyframe", animationId, percentage, properties },
{ label: `Add keyframe at ${percentage}%`, softReload: true, shadowKeyframeOp },
{ label: `Add keyframe at ${percentage}%`, softReload: true },
);
},
[commitMutation],
Expand All @@ -114,10 +99,6 @@ export function useGsapKeyframeOps({
(selection: DomEditSelection, animationId: string, percentage: number) => {
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
const mutation = { type: "remove-keyframe", animationId, percentage };
// Shadow op (gsap_keyframe): SDK has no %-based removeGsapKeyframe on main,
// so the runner resolves percentage → keyframeIndex against the pre-op
// script and no-ops on ambiguity (duplicate-percentage keyframes).
const shadowKeyframeOp: ShadowKeyframeOp = { kind: "remove", animationId, percentage };
void executeOptimisticKeyframeCacheUpdate({
sourceFile,
elementId: selection.id,
Expand All @@ -131,7 +112,6 @@ export function useGsapKeyframeOps({
commitMutation(selection, mutation, {
label: `Remove keyframe at ${percentage}%`,
softReload: true,
shadowKeyframeOp,
}),
}).catch((error) => {
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
Expand Down
43 changes: 7 additions & 36 deletions packages/studio/src/hooks/useGsapScriptCommits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { useCallback, useRef } from "react";
import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { applySoftReload } from "../utils/gsapSoftReload";
import { resolveGsapFidelityArgs, runShadowGsapFidelity } from "../utils/sdkShadowGsapFidelity";
import { runShadowGsapKeyframeFidelity } from "../utils/sdkShadowGsapKeyframe";
import { updateKeyframeCacheFromParsed } from "./gsapKeyframeCacheHelpers";
import { createKeyedSerializer } from "./serializeByKey";
import {
Expand Down Expand Up @@ -46,15 +44,12 @@ async function mutateGsapScript(

// oxfmt-ignore
// fallow-ignore-next-line complexity
export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession }: GsapScriptCommitsParams) {
export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast }: GsapScriptCommitsParams) {
// Serializer for per-key commits (options.serializeKey). Keyed by
// `gsap:${animationId}:meta`, it chains a meta commit onto the prior one for
// the same animationId so their POSTs can't interleave — which is what made
// the shadow fidelity diff pair an op with a stale server result and report
// false ease mismatches. Held in a ref so the chain survives re-renders.
// the same animationId so their POSTs can't interleave. Held in a ref so the
// chain survives re-renders.
const serializerRef = useRef(createKeyedSerializer());
// Pre-existing complexity (server mutate + history + reload branches); this PR
// adds only a guarded shadow-fidelity dispatch.
// fallow-ignore-next-line complexity
const runCommit = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
const pid = projectIdRef.current;
Expand All @@ -76,28 +71,6 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
}
if (result.changed === false) return;
domEditSaveTimestampRef.current = Date.now();
// Shadow value fidelity: diff the SDK's GSAP writer output against the
// server's, from the same pre-op file. Fire-and-forget; server authoritative.
// Meta-level ops carry shadowGsapOp (add / update-meta / delete via
// useGsapAnimationOps); keyframe ops carry shadowKeyframeOp (add/remove via
// useGsapKeyframeOps, handled by the gsap_keyframe block below). Per-property
// handlers (useGsapPropertyDebounce) don't synthesize one yet — deferred follow-up.
// scriptText is null when the composition has no GSAP script; nothing to diff.
const fidelityArgs = resolveGsapFidelityArgs(
sdkSession,
options.shadowGsapOp,
result.before,
result.scriptText,
);
if (fidelityArgs) {
void runShadowGsapFidelity(fidelityArgs.before, fidelityArgs.op, fidelityArgs.serverScript);
}
// Keyframe value fidelity (gsap_keyframe): same serialize-diff approach, but
// the SDK has no keyframe reader so there is no live-existence path — the diff
// is the only signal. Guarded on a live session + both scripts to diff.
if (sdkSession && options.shadowKeyframeOp && result.before != null && result.scriptText != null) {
void runShadowGsapKeyframeFidelity(result.before, options.shadowKeyframeOp, result.scriptText);
}
if (result.before != null && result.after != null) {
await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
}
Expand All @@ -111,12 +84,10 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
reloadPreview();
}
onCacheInvalidate();
}, [projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession]);
}, [projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast]);
// Every GSAP-script commit is a read-modify-write of one file. Overlapping
// commits to the SAME file (any op type, any animation) interleave server-side
// and make the shadow fidelity diff pair an op with a stale server result —
// the false ease/value mismatches this serializer exists to prevent. So
// serialize per target file by default; an explicit serializeKey overrides.
// commits to the SAME file (any op type, any animation) interleave server-side,
// so serialize per target file by default; an explicit serializeKey overrides.
const commitMutation = useCallback(
(selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
const file = selection.sourceFile || activeCompPath || "index.html";
Expand All @@ -128,7 +99,7 @@ export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIfra
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToast);
const propertyOps = useGsapPropertyDebounce(commitMutationSafely);
const animationOps = useGsapAnimationOps({ projectIdRef, activeCompPath, commitMutation, commitMutationSafely, showToast, sdkSession });
const animationOps = useGsapAnimationOps({ projectIdRef, activeCompPath, commitMutation, commitMutationSafely, showToast });
const keyframeOps = useGsapKeyframeOps({ activeCompPath, commitMutation, commitMutationSafely, trackGsapSaveFailure });
const arcPathOps = useGsapArcPathOps(commitMutationSafely);
return { commitMutation, ...propertyOps, ...animationOps, ...keyframeOps, ...arcPathOps };
Expand Down
Loading
Loading