Skip to content

fix(studio): force-reload sdk session after undo/redo bypasses suppress window - #1460

Closed
vanceingalls wants to merge 45 commits into
sdk-stage7-s7step3c-cleanfrom
sdk-stage7-studio-s7step4
Closed

vanceingalls wants to merge 45 commits into
sdk-stage7-s7step3c-cleanfrom
sdk-stage7-studio-s7step4

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fixes a stale-session bug after undo/redo: after a successful undo, the SDK in-memory document was left on the pre-undo (post-edit) content instead of the reverted content.

Why

writeHistoryFile (called by undo/redo to write the before-content to disk) sets domEditSaveTimestampRef.current = Date.now(), which arms the 2-second self-write suppress window in useSdkSession. That suppress window exists to prevent echo-reloads after SDK cutover writes, but it also swallows the file-change event triggered by the undo write — so the SDK session never reloaded to reflect the reverted file.

How

  • useSdkSession now returns SdkSessionHandle { session, forceReload } instead of Composition | null
  • forceReload() bumps reloadToken directly, bypassing the suppress window entirely
  • useAppHotkeys.applyHistory calls forceReloadSdkSession() after a successful undo/redo that includes the active composition path in result.paths
  • App.tsx passes activeCompPath and forceReloadSdkSession into useAppHotkeys

Test plan

  • useSdkSession.test.ts: shouldReloadSdkSession pure function tests (path match, no active comp, no path in payload)
  • Integration covered by usePersistentEditHistory.test.ts — verifies undo writes correct before-content to disk; the force-reload path is React hook internals not unit-testable without a render environment

vanceingalls and others added 30 commits June 14, 2026 21:55
…rops

onStart, onUpdate, onRepeat were missing from the assertion; only onComplete
was checked. All four are listed in DROPPED_VAR_KEYS so all four belong here.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- gsapParserAcorn: top-level variable targets now resolved via program-scope
  null-key fallback in lookupBindingFromAncestors (const el = querySelector...)
- gsapParserAcorn: fromTo guard requires args.length >= 3, preventing undefined
  args[2]/args[3] access when fewer args supplied
- gsapWriterAcorn: remove fuzzing fallback in removeAnimationFromScript that
  silently deleted the wrong animation (from→to ID conversion)
- gsapWriterAcorn: valueToCode guards NaN → "0" to avoid broken tween props;
  safeKey regex aligned to ASCII-only (matching gsapSerialize)
- mutate: handleSetGsapTween now includes stagger in extras (was in addGsapTween
  but missing from setGsapTween)
- apply-patches: script case now mirrors stylesheet — op=remove calls
  setGsapScript("") instead of silently ignoring the patch

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Documents the recast-baseline trust relationship and clarifies that
motionPath parity tests live in the Phase 3b commit (PR #1379) since
the acorn motionPath parser is also added there.

Addresses #1370 R1-N1 (Rames).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
- setGsapScript: remove element when newScript="" (fixes undo/redo duplicate-script bug)
- parseDeclarations: track quotes so ; inside CSS values (data URIs) doesn't split
- handleRemoveGsapKeyframe: guard against duplicate-percentage ambiguity (return EMPTY)
- resolveKeyframe: return kfs so callers can check uniqueness
- handleSetClassStyle: emit op:"add" (not "replace") when no prior <style> element
- FsAdapter listVersions: Number(f.split("_")[0]) — was NaN due to underscore in key
- FsAdapter doWrite: split try/catch so appendVersion failure doesn't fire error handlers
- FileAdapter playground: add content:"" field to satisfy PersistVersionEntry contract

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… result.code

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
… scoped ids

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
Expose the concrete adapter factories so consumers no longer reach into deep
adapter paths:
- createHeadlessAdapter — no-op PreviewAdapter for agents/CI/SSR (no browser)
- createMemoryAdapter — in-memory PersistAdapter for tests/headless open
- createFsAdapter (+ FsAdapterOptions) — node fs PersistAdapter for local dev

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds fully-qualified scoped ids for addressing elements inside inlined
sub-compositions, so callers can target "hf-HOST/hf-LEAF" unambiguously
even when bare hf-ids collide across sub-composition boundaries.

Changes:
- model.ts: resolveScoped() traverses id segments through nested subtrees;
  isNewHostBoundary() detects host boundaries (dcf ≠ parent dcf handles
  outerHTML innerRoot edge case)
- types.ts: HyperFramesElement gains scopedId field
- document.ts: buildElement carries scopePrefix, propagates childPrefix
  at host boundaries; buildRoots starts with ""
- patches.ts: RFC 6902 escapeIdForPath / decodePathSegment for scoped ids
  containing "/"; all path builders and pathToKey/keyToPath updated
- session.ts: getElement() matches by scopedId; find() returns scopedIds;
  orphan cleanup decodes RFC 6902 before key comparison, preserves removal
  markers, purges property sub-keys for both bare and scoped ids
- mutate.ts: all element handlers use resolveScoped instead of findById;
  handleRemoveElement collects full subtree hf-ids before removal for
  complete GSAP animation cascade (Q3 fix); validateOp uses resolveScoped

20 new contract tests in session.subcomp.test.ts covering resolveScoped,
scopedId propagation, dispatch to scoped targets, RFC 6902 patch encoding,
override-set key format, orphan purge, and serialize stability.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Closes the last headless-testable Stage 6 gap (F9 workstream C).

`find({ composition: "hf-host" })` returns all scopedIds whose prefix
matches the given host id — i.e. every element mounted inside that
sub-composition, at any depth. Combinable with other FindQuery fields
(tag, text, name, track). 3 new contract tests in session.subcomp.test.ts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Browser-fetch-based PersistAdapter for Studio REST file API.
Uses fetch-only (no node:fs) so it is safe to bundle in Vite.

  - packages/sdk/src/adapters/http.ts — HttpAdapter class + createHttpAdapter factory
  - packages/sdk/src/adapters/http.test.ts — 14 unit tests (fetch mocked via vi.stubGlobal)
  - packages/sdk/src/index.ts — re-export createHttpAdapter + HttpAdapterOptions
  - packages/sdk/package.json — ./adapters/http subpath export (dev + publishConfig)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add per-path promise queue so rapid successive writes to the same
composition file cannot race at the server. Different paths still
write concurrently. Two new tests (RED→GREEN verified).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…lush-drains-two test

- Add optional headers?: HeadersInit | (() => HeadersInit) to HttpAdapterOptions
  for cross-origin / CLI / auth injection (function form refreshes on each PUT)
- Document listVersions/loadFrom as intentional no-ops (server versioning not exposed)
- Add flush-drains-two-concurrent-writes test to mirror fs adapter T13 coverage

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
Adds setSelection(ids: string[]) to Composition interface and CompositionImpl.
Fires selectionchange; does not touch undo stack or patch stream.
11 contract tests: get/set/clear, event firing, copy semantics, no undo/patch side-effects.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Skip event dispatch when ids are identical (same length, same order)
to prevent double-firing selectionchange from callers that call
setSelection with the same list. Two new tests (RED→GREEN verified).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
Reviewer noted it is dead surface in this stack — no caller uses it.
Add comment explaining it is wired up in stage 8 when the preview host
pushes selection events up to the SDK session.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

Co-authored-by: Miguel Ángel <[email protected]>
Creates useSdkSession hook: fetches active composition HTML, opens an
SDK Composition backed by createHttpAdapter, disposes on comp/project change.
Session is idle (no dispatch routed yet) — Step 3 wires edit ops through it.

Also removes createFsAdapter from SDK main entry (Node-only; subpath-only:
@hyperframes/sdk/adapters/fs). Required for Studio typecheck to pass when
importing @hyperframes/sdk — fs.ts uses node:fs/promises which Studio's
tsconfig does not include.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
useSdkSelectionSync: effect that calls session.setSelection(hfIds) whenever
domEditSelection or domEditGroupSelections changes. Maps each entry's hfId;
skips entries without one. Pure additive — no existing hook modified.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Build the HttpAdapter first, then call adapter.read(activeCompPath)
instead of duplicating URL construction with a raw fetch. Eliminates
the /files/encode duplication already in HttpAdapter.read().

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@vanceingalls
vanceingalls changed the base branch from sdk-stage7-studio-s7step3c to graphite-base/1460 June 15, 2026 18:48
@vanceingalls
vanceingalls changed the base branch from graphite-base/1460 to main June 15, 2026 18:48
@vanceingalls
vanceingalls changed the base branch from main to graphite-base/1460 June 16, 2026 05:53
@vanceingalls
vanceingalls changed the base branch from graphite-base/1460 to main June 16, 2026 05:53
@vanceingalls
vanceingalls changed the base branch from main to graphite-base/1460 June 16, 2026 05:54
@vanceingalls
vanceingalls changed the base branch from graphite-base/1460 to sdk-stage7-studio-s7step3c June 16, 2026 05:55
@vanceingalls
vanceingalls changed the base branch from sdk-stage7-studio-s7step3c to sdk-stage7-s7step3c-clean June 17, 2026 01:37
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Superseded by clean squash off sdk-stage7-s7step3c-clean (#1522)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant