From fff2884a4268a1615dddadac2187f5fef6fdd857 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 14 Jun 2026 14:41:02 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(studio):=20stage=207=20step=201=20?= =?UTF-8?q?=E2=80=94=20wire=20SDK=20session=20into=20Studio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/sdk/src/index.ts | 4 +- packages/studio/package.json | 1 + packages/studio/src/App.tsx | 3 ++ packages/studio/src/hooks/useSdkSession.ts | 53 ++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 packages/studio/src/hooks/useSdkSession.ts diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 57ef673c7..682b217bc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -33,10 +33,8 @@ export type { PersistQueueModule, PersistQueueOptions } from "./persist-queue.js export type { PersistAdapter, PreviewAdapter, PersistVersionEntry } from "./adapters/types.js"; -// Concrete adapter factories. +// Concrete adapter factories (browser-safe — Node-only fs adapter: @hyperframes/sdk/adapters/fs). export { createMemoryAdapter } from "./adapters/memory.js"; export { createHeadlessAdapter } from "./adapters/headless.js"; -export { createFsAdapter } from "./adapters/fs.js"; -export type { FsAdapterOptions } from "./adapters/fs.js"; export { createHttpAdapter } from "./adapters/http.js"; export type { HttpAdapterOptions } from "./adapters/http.js"; diff --git a/packages/studio/package.json b/packages/studio/package.json index 7173b9a7f..125cb31ca 100644 --- a/packages/studio/package.json +++ b/packages/studio/package.json @@ -38,6 +38,7 @@ "@codemirror/view": "6.40.0", "@hyperframes/core": "workspace:*", "@hyperframes/player": "workspace:*", + "@hyperframes/sdk": "workspace:*", "@phosphor-icons/react": "^2.1.10", "mediabunny": "^1.45.3" }, diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 2f944b228..14c7f9e06 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -12,6 +12,7 @@ import { usePreviewPersistence } from "./hooks/usePreviewPersistence"; import { useTimelineEditing } from "./hooks/useTimelineEditing"; import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab"; import { useDomEditSession } from "./hooks/useDomEditSession"; +import { useSdkSession } from "./hooks/useSdkSession"; import { useBlockHandlers } from "./hooks/useBlockHandlers"; import { useAppHotkeys } from "./hooks/useAppHotkeys"; import { useClipboard } from "./hooks/useClipboard"; @@ -145,6 +146,8 @@ export function StudioApp() { setRefreshKey, }); + const _sdkSession = useSdkSession(projectId, activeCompPath); + useEffect(() => { if (activeCompPathHydrated) return; if (!fileManager.fileTreeLoaded) return; diff --git a/packages/studio/src/hooks/useSdkSession.ts b/packages/studio/src/hooks/useSdkSession.ts new file mode 100644 index 000000000..e9cf7256d --- /dev/null +++ b/packages/studio/src/hooks/useSdkSession.ts @@ -0,0 +1,53 @@ +import { useState, useEffect } from "react"; +import { openComposition } from "@hyperframes/sdk"; +import { createHttpAdapter } from "@hyperframes/sdk/adapters/http"; +import type { Composition } from "@hyperframes/sdk"; + +/** + * Stage 7 Step 1 — SDK session wired to the active composition. + * + * Creates an SDK Composition backed by createHttpAdapter on every + * (projectId, activeCompPath) change, disposes the old one on cleanup. + * The session is idle until Step 3 routes dispatch ops through it. + */ +export function useSdkSession( + projectId: string | null, + activeCompPath: string | null, +): Composition | null { + const [session, setSession] = useState(null); + + useEffect(() => { + if (!projectId || !activeCompPath) { + setSession(null); + return; + } + + let cancelled = false; + let comp: Composition | null = null; + + const url = `/api/projects/${projectId}/files/${encodeURIComponent(activeCompPath)}?optional=1`; + fetch(url) + .then((r) => r.json()) + .then(async (data: { content?: string }) => { + if (cancelled || typeof data.content !== "string") return; + const adapter = createHttpAdapter({ + projectFilesUrl: `/api/projects/${projectId}`, + }); + comp = await openComposition(data.content, { persist: adapter }); + comp.on("persist:error", (e) => { + console.warn("[sdk] persist:error", e.error); + }); + if (!cancelled) setSession(comp); + }) + .catch(() => { + if (!cancelled) setSession(null); + }); + + return () => { + cancelled = true; + comp?.dispose(); + }; + }, [projectId, activeCompPath]); + + return session; +} From 6112421ea4e1cd37086cc7cc9565c116d59eff23 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 14 Jun 2026 14:51:42 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(studio):=20stage=207=20step=202=20?= =?UTF-8?q?=E2=80=94=20mirror=20canvas=20selection=20into=20SDK=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/studio/src/App.tsx | 9 ++++++- .../studio/src/hooks/useSdkSelectionSync.ts | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/studio/src/hooks/useSdkSelectionSync.ts diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 14c7f9e06..b884e726b 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -13,6 +13,7 @@ import { useTimelineEditing } from "./hooks/useTimelineEditing"; import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab"; import { useDomEditSession } from "./hooks/useDomEditSession"; import { useSdkSession } from "./hooks/useSdkSession"; +import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync"; import { useBlockHandlers } from "./hooks/useBlockHandlers"; import { useAppHotkeys } from "./hooks/useAppHotkeys"; import { useClipboard } from "./hooks/useClipboard"; @@ -146,7 +147,7 @@ export function StudioApp() { setRefreshKey, }); - const _sdkSession = useSdkSession(projectId, activeCompPath); + const sdkSession = useSdkSession(projectId, activeCompPath); useEffect(() => { if (activeCompPathHydrated) return; @@ -317,6 +318,12 @@ export function StudioApp() { if (Number.isFinite(p)) domEditSession.handleGsapRemoveKeyframe(a.id, p); }); }; + useSdkSelectionSync( + sdkSession, + domEditSession.domEditSelection, + domEditSession.domEditGroupSelections, + ); + useCaptionDetection({ projectId, activeCompPath, diff --git a/packages/studio/src/hooks/useSdkSelectionSync.ts b/packages/studio/src/hooks/useSdkSelectionSync.ts new file mode 100644 index 000000000..d36cfdabe --- /dev/null +++ b/packages/studio/src/hooks/useSdkSelectionSync.ts @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import type { Composition } from "@hyperframes/sdk"; +import type { DomEditSelection } from "../components/editor/domEditing"; + +function toHfIds(group: DomEditSelection[], primary: DomEditSelection | null): string[] { + const source = group.length > 0 ? group : primary ? [primary] : []; + return source.flatMap((s) => (s.hfId ? [s.hfId] : [])); +} + +/** + * Stage 7 Step 2 — mirrors Studio canvas selection into the SDK session. + * + * Calls session.setSelection(hfIds) whenever domEditSelection or + * domEditGroupSelections changes. Pure effect; no existing hook modified. + */ +export function useSdkSelectionSync( + session: Composition | null, + domEditSelection: DomEditSelection | null, + domEditGroupSelections: DomEditSelection[], +): void { + useEffect(() => { + if (!session) return; + session.setSelection(toHfIds(domEditGroupSelections, domEditSelection)); + }, [session, domEditSelection, domEditGroupSelections]); +}