@hyperframes/studio 0.8.19 → 0.8.21
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.
- package/dist/assets/hyperframes-player-CIXNA_bj.js +460 -0
- package/dist/assets/{index-Dd0fsIIL.js → index-44gfDvkh.js} +1 -1
- package/dist/assets/{index-DxccOrkZ.js → index-9VfE7bZe.js} +1 -1
- package/dist/assets/{index-Bjd2hioj.js → index-CgGY8dAv.js} +164 -164
- package/dist/index.d.ts +28 -0
- package/dist/index.html +1 -1
- package/dist/index.js +1206 -234
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -0
- package/src/contexts/StudioContext.tsx +10 -0
- package/src/hooks/useEditorSave.test.tsx +81 -3
- package/src/hooks/useEditorSave.ts +31 -6
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +48 -16
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -2
- package/src/hooks/useStudioContextValue.ts +9 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +96 -0
- package/src/player/hooks/timelineSyncHydration.ts +47 -1
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +52 -29
- package/src/player/hooks/useTimelineSyncCallbacks.ts +4 -0
- package/src/player/store/playerStore.ts +17 -2
- package/src/player/store/timelineElement.ts +21 -0
- package/src/utils/studioFileVersion.test.ts +37 -1
- package/src/utils/studioFileVersion.ts +12 -2
- package/src/utils/studioSaveDiagnostics.test.ts +29 -0
- package/src/utils/studioSaveDiagnostics.ts +4 -0
- package/src/webmcp/StudioAgentTools.tsx +105 -10
- package/src/webmcp/toolResult.ts +1 -1
- package/src/webmcp/tools/animationTools.test.ts +244 -0
- package/src/webmcp/tools/animationTools.ts +276 -0
- package/src/webmcp/tools/contentTools.test.ts +275 -0
- package/src/webmcp/tools/contentTools.ts +217 -0
- package/src/webmcp/tools/frameTools.test.ts +136 -0
- package/src/webmcp/tools/frameTools.ts +133 -0
- package/src/webmcp/tools/inspectTools.test.ts +197 -0
- package/src/webmcp/tools/inspectTools.ts +208 -0
- package/src/webmcp/tools/selectionTools.test.ts +164 -0
- package/src/webmcp/tools/selectionTools.ts +154 -0
- package/src/webmcp/tools/transformTools.test.ts +179 -0
- package/src/webmcp/tools/transformTools.ts +205 -0
- package/src/webmcp/useStudioAgentTools.test.tsx +71 -22
- package/src/webmcp/useStudioAgentTools.ts +195 -1
- package/src/webmcp/webmcpTestUtils.ts +91 -0
- package/dist/assets/hyperframes-player-BA-QO9CQ.js +0 -459
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
usePlayerStore,
|
|
4
|
+
type TimelineElement,
|
|
5
|
+
type DomClipChild,
|
|
6
|
+
type SubCompositionHostState,
|
|
7
|
+
} from "../store/playerStore";
|
|
3
8
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
4
9
|
import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
|
|
5
10
|
import { buildTimelineElementKey, splitTimelineElementKey } from "../lib/timelineElementHelpers";
|
|
@@ -131,18 +136,6 @@ interface DisplayBounds {
|
|
|
131
136
|
track: number;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
/**
|
|
135
|
-
* State that lives on the live host element, not in the clip manifest:
|
|
136
|
-
* `data-hidden`, `data-timeline-locked`, `data-timeline-role`. A child row is
|
|
137
|
-
* built from a manifest clip with no hostEl to read, so
|
|
138
|
-
* createTimelineElementFromManifestClip cannot see any of it. The flat store
|
|
139
|
-
* element for the same child WAS built with one, so it is inherited from there.
|
|
140
|
-
*
|
|
141
|
-
* Without this the eye on an expanded child always reported the row visible, so
|
|
142
|
-
* clicking it wrote data-hidden again instead of removing it, and a hidden child
|
|
143
|
-
* could never be shown again (not even after a reload, since the attribute is in
|
|
144
|
-
* the source).
|
|
145
|
-
*/
|
|
146
139
|
/**
|
|
147
140
|
* Audio-group membership for an expanded child, from whichever source has it.
|
|
148
141
|
*
|
|
@@ -169,20 +162,34 @@ function childGroupState(
|
|
|
169
162
|
};
|
|
170
163
|
}
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
165
|
+
/**
|
|
166
|
+
* State that lives on the live host element, not in the clip manifest:
|
|
167
|
+
* `data-hidden`, `data-timeline-locked`, `data-timeline-role`, `data-fx-chain`,
|
|
168
|
+
* `data-automation`. A child row is built from a manifest clip with no hostEl to
|
|
169
|
+
* read, so createTimelineElementFromManifestClip cannot see any of it.
|
|
170
|
+
*
|
|
171
|
+
* `live` is the reading taken off the element itself and is authoritative. For a
|
|
172
|
+
* child of a REAL sub-composition it is also the only source: such a clip is
|
|
173
|
+
* filtered out of the manifest before the flat store is built, so no twin exists
|
|
174
|
+
* to inherit from. The flat twin still covers the phantom-wrapper case, where
|
|
175
|
+
* the child does keep a store entry of its own.
|
|
176
|
+
*
|
|
177
|
+
* Without a reading of the element, the eye on an expanded child always reported
|
|
178
|
+
* the row visible, so clicking it wrote data-hidden again instead of removing
|
|
179
|
+
* it, and a hidden child could never be shown again (not even after a reload,
|
|
180
|
+
* since the attribute is in the source). Missing fxChain and automation, an
|
|
181
|
+
* audio child inside a sub-composition reserved no automation height and drew no
|
|
182
|
+
* lanes, while the property panel, which reads the live DOM selection rather
|
|
183
|
+
* than this row, still showed the chain and its toggles.
|
|
184
|
+
*/
|
|
185
|
+
function hostElementState(
|
|
186
|
+
flat: TimelineElement | undefined,
|
|
187
|
+
live: SubCompositionHostState | undefined,
|
|
188
|
+
): Partial<TimelineElement> {
|
|
189
|
+
if (!flat) return { ...live };
|
|
190
|
+
const { hidden, timelineLocked, timelineRole, fxChain, automation } = flat;
|
|
191
|
+
// `live` last: it is the reading off the element, so it wins wherever it has one.
|
|
192
|
+
return { hidden, timelineLocked, timelineRole, fxChain, automation, ...live };
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
// `display` bounds come from the top-level scene clip (where the expanded row is
|
|
@@ -196,6 +203,7 @@ function buildChildElements(
|
|
|
196
203
|
expandedHostKey: string,
|
|
197
204
|
elements: readonly TimelineElement[],
|
|
198
205
|
domChildrenById: ReadonlyMap<string, DomClipChild>,
|
|
206
|
+
hostStateById: ReadonlyMap<string, SubCompositionHostState>,
|
|
199
207
|
): TimelineElement[] {
|
|
200
208
|
const result: TimelineElement[] = [];
|
|
201
209
|
for (const child of siblings) {
|
|
@@ -223,7 +231,10 @@ function buildChildElements(
|
|
|
223
231
|
});
|
|
224
232
|
result.push({
|
|
225
233
|
...base,
|
|
226
|
-
...hostElementState(
|
|
234
|
+
...hostElementState(
|
|
235
|
+
elements.find((element) => element.key === key),
|
|
236
|
+
domId ? hostStateById.get(domId) : undefined,
|
|
237
|
+
),
|
|
227
238
|
...childGroupState(
|
|
228
239
|
elements.find((element) => element.key === key),
|
|
229
240
|
domId ? domChildrenById.get(domId) : undefined,
|
|
@@ -309,6 +320,7 @@ export function buildExpandedElements(
|
|
|
309
320
|
topLevelId: string,
|
|
310
321
|
siblingParentId: string,
|
|
311
322
|
domClipChildren: DomClipChild[] = [],
|
|
323
|
+
subCompositionHostState: ReadonlyMap<string, SubCompositionHostState> = new Map(),
|
|
312
324
|
): TimelineElement[] {
|
|
313
325
|
const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
|
|
314
326
|
if (!topLevelElement) return filterToTopLevel(elements, parentMap);
|
|
@@ -348,6 +360,7 @@ export function buildExpandedElements(
|
|
|
348
360
|
parentKey,
|
|
349
361
|
elements,
|
|
350
362
|
domChildrenById,
|
|
363
|
+
subCompositionHostState,
|
|
351
364
|
);
|
|
352
365
|
if (expanded.length === 0) return filterToTopLevel(elements, parentMap);
|
|
353
366
|
|
|
@@ -391,6 +404,7 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
391
404
|
const clipManifest = usePlayerStore((s) => s.clipManifest);
|
|
392
405
|
const clipParentMap = usePlayerStore((s) => s.clipParentMap);
|
|
393
406
|
const domClipChildren = usePlayerStore((s) => s.domClipChildren);
|
|
407
|
+
const subCompositionHostState = usePlayerStore((s) => s.subCompositionHostState);
|
|
394
408
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
395
409
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
396
410
|
|
|
@@ -432,6 +446,15 @@ export function useExpandedTimelineElements(): TimelineElement[] {
|
|
|
432
446
|
topLevel,
|
|
433
447
|
immediateParent,
|
|
434
448
|
domClipChildren,
|
|
449
|
+
subCompositionHostState,
|
|
435
450
|
);
|
|
436
|
-
}, [
|
|
451
|
+
}, [
|
|
452
|
+
elements,
|
|
453
|
+
clipManifest,
|
|
454
|
+
clipParentMap,
|
|
455
|
+
domClipChildren,
|
|
456
|
+
subCompositionHostState,
|
|
457
|
+
rawId,
|
|
458
|
+
selectedRawId,
|
|
459
|
+
]);
|
|
437
460
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
buildTimelineElementsFromClips,
|
|
20
20
|
clipTreeParentMap,
|
|
21
21
|
collectSubCompositionDomChildren,
|
|
22
|
+
collectSubCompositionHostState,
|
|
22
23
|
hydrateTimelineFromPreview,
|
|
23
24
|
isPreviewReadinessMessage,
|
|
24
25
|
safeContentDocument,
|
|
@@ -131,6 +132,9 @@ export function useTimelineSyncCallbacks({
|
|
|
131
132
|
const domClipChildren = collectSubCompositionDomChildren(iframeDoc, data.clips, parentMap);
|
|
132
133
|
usePlayerStore.getState().setClipParentMap(parentMap);
|
|
133
134
|
usePlayerStore.getState().setDomClipChildren(domClipChildren);
|
|
135
|
+
usePlayerStore
|
|
136
|
+
.getState()
|
|
137
|
+
.setSubCompositionHostState(collectSubCompositionHostState(iframeDoc, data.clips));
|
|
134
138
|
} catch {
|
|
135
139
|
// cross-origin or __clipTree not available — maps stay empty
|
|
136
140
|
}
|
|
@@ -23,9 +23,13 @@ import { createThumbnailSlice, type ThumbnailSlice } from "./thumbnailSlice";
|
|
|
23
23
|
export type { KeyframeCacheEntry } from "./keyframeSlice";
|
|
24
24
|
export { liveTime } from "./liveTime";
|
|
25
25
|
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
TimelineElement,
|
|
28
|
+
TimelineElementPatch,
|
|
29
|
+
SubCompositionHostState,
|
|
30
|
+
} from "./timelineElement";
|
|
27
31
|
|
|
28
|
-
export type { TimelineElement };
|
|
32
|
+
export type { TimelineElement, SubCompositionHostState };
|
|
29
33
|
export type ZoomMode = "fit" | "manual";
|
|
30
34
|
type TimelineTool = "select" | "razor";
|
|
31
35
|
|
|
@@ -212,6 +216,14 @@ interface PlayerState
|
|
|
212
216
|
*/
|
|
213
217
|
domClipChildren: DomClipChild[];
|
|
214
218
|
setDomClipChildren: (children: DomClipChild[]) => void;
|
|
219
|
+
/**
|
|
220
|
+
* Host-element state for every id'd element inside a sub-composition, keyed by
|
|
221
|
+
* dom id. Collected from the live preview because it is the only place that
|
|
222
|
+
* sees it: these elements are filtered out of `elements` before the flat store
|
|
223
|
+
* is built, and the clip manifest carries timing, not attributes.
|
|
224
|
+
*/
|
|
225
|
+
subCompositionHostState: Map<string, SubCompositionHostState>;
|
|
226
|
+
setSubCompositionHostState: (state: Map<string, SubCompositionHostState>) => void;
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
/** A sub-comp DOM-only timeline child (no data-start) and its nesting context. */
|
|
@@ -284,6 +296,7 @@ export function createTimelineResetState() {
|
|
|
284
296
|
clipManifest: null,
|
|
285
297
|
clipParentMap: new Map<string, string>(),
|
|
286
298
|
domClipChildren: [],
|
|
299
|
+
subCompositionHostState: new Map<string, SubCompositionHostState>(),
|
|
287
300
|
};
|
|
288
301
|
}
|
|
289
302
|
|
|
@@ -430,6 +443,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
430
443
|
setClipParentMap: (map) => set({ clipParentMap: map }),
|
|
431
444
|
domClipChildren: [],
|
|
432
445
|
setDomClipChildren: (children) => set({ domClipChildren: children }),
|
|
446
|
+
subCompositionHostState: new Map(),
|
|
447
|
+
setSubCompositionHostState: (state) => set({ subCompositionHostState: state }),
|
|
433
448
|
|
|
434
449
|
setIsPlaying: (playing) => {
|
|
435
450
|
if (get().isPlaying === playing) return;
|
|
@@ -118,3 +118,24 @@ export type TimelineElementPatch = Partial<
|
|
|
118
118
|
| "audioGroupAutomation"
|
|
119
119
|
>
|
|
120
120
|
>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The `data-*` state an expanded sub-composition child needs but cannot reach.
|
|
124
|
+
*
|
|
125
|
+
* A child row is synthesized from a manifest clip with no element to read, and
|
|
126
|
+
* for a real sub-composition it has no flat store twin either: such clips are
|
|
127
|
+
* dropped before the flat store is built. Read off the live preview instead
|
|
128
|
+
* (`collectSubCompositionHostState`) and carried on the store by dom id.
|
|
129
|
+
*
|
|
130
|
+
* Without it the eye reported every hidden child visible, so clicking it wrote
|
|
131
|
+
* `data-hidden` a second time instead of removing it, and the element could
|
|
132
|
+
* never be shown again, not even after a reload, since the attribute is in the
|
|
133
|
+
* source.
|
|
134
|
+
*/
|
|
135
|
+
export interface SubCompositionHostState {
|
|
136
|
+
hidden?: boolean;
|
|
137
|
+
timelineLocked?: boolean;
|
|
138
|
+
timelineRole?: string;
|
|
139
|
+
fxChain?: string;
|
|
140
|
+
automation?: string;
|
|
141
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
consumeStudioWriteToken,
|
|
4
|
+
createStudioWriteToken,
|
|
4
5
|
markStudioWriteToken,
|
|
5
6
|
resetStudioWriteTokens,
|
|
6
7
|
studioExpectedFileVersion,
|
|
7
8
|
studioFileContentVersion,
|
|
9
|
+
studioWriteHeaders,
|
|
8
10
|
} from "./studioFileVersion";
|
|
9
11
|
|
|
12
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
13
|
+
|
|
10
14
|
describe("studioFileContentVersion", () => {
|
|
11
15
|
it("matches the strong SHA-256 ETag format used by studio-server", async () => {
|
|
12
16
|
await expect(studioFileContentVersion("abc")).resolves.toBe(
|
|
@@ -42,6 +46,38 @@ describe("studioFileContentVersion", () => {
|
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
describe("studio write-token echo identity", () => {
|
|
49
|
+
it("prefers the platform randomUUID implementation", () => {
|
|
50
|
+
const randomUUID = vi.fn(() => "11111111-2222-4333-8444-555555555555");
|
|
51
|
+
vi.stubGlobal("crypto", { randomUUID });
|
|
52
|
+
resetStudioWriteTokens();
|
|
53
|
+
|
|
54
|
+
expect(studioWriteHeaders()).toEqual({
|
|
55
|
+
"X-Hyperframes-Write-Token": "11111111-2222-4333-8444-555555555555",
|
|
56
|
+
});
|
|
57
|
+
expect(randomUUID).toHaveBeenCalledOnce();
|
|
58
|
+
expect(consumeStudioWriteToken("11111111-2222-4333-8444-555555555555")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("creates an RFC 4122 UUID-v4 token from getRandomValues when randomUUID is unavailable", () => {
|
|
62
|
+
const source = Uint8Array.from({ length: 16 }, (_, index) => index);
|
|
63
|
+
vi.stubGlobal("crypto", {
|
|
64
|
+
getRandomValues: vi.fn((target: Uint8Array) => {
|
|
65
|
+
target.set(source);
|
|
66
|
+
return target;
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(createStudioWriteToken()).toBe("00010203-0405-4607-8809-0a0b0c0d0e0f");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("fails explicitly when Web Crypto cannot provide secure random bytes", () => {
|
|
74
|
+
vi.stubGlobal("crypto", {});
|
|
75
|
+
|
|
76
|
+
expect(() => createStudioWriteToken()).toThrow(
|
|
77
|
+
"Web Crypto getRandomValues is required for Studio write identity",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
45
81
|
it("suppresses exactly one matching API write receipt without hiding path-only external writes", () => {
|
|
46
82
|
resetStudioWriteTokens();
|
|
47
83
|
markStudioWriteToken("studio-write-1");
|
|
@@ -48,8 +48,18 @@ export async function studioExpectedFileVersion(
|
|
|
48
48
|
return versions.get(path);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function createStudioWriteToken(): string {
|
|
52
|
-
|
|
51
|
+
export function createStudioWriteToken(): string {
|
|
52
|
+
const webCrypto = globalThis.crypto;
|
|
53
|
+
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
|
54
|
+
if (typeof webCrypto?.getRandomValues !== "function") {
|
|
55
|
+
throw new Error("Web Crypto getRandomValues is required for Studio write identity");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const bytes = webCrypto.getRandomValues(new Uint8Array(16));
|
|
59
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
60
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
61
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const trackStudioEvent = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock("./studioTelemetry", () => ({ trackStudioEvent }));
|
|
5
|
+
|
|
2
6
|
import {
|
|
3
7
|
StudioFileConflictError,
|
|
4
8
|
StudioSaveHttpError,
|
|
@@ -6,6 +10,7 @@ import {
|
|
|
6
10
|
buildStudioSaveFailureProperties,
|
|
7
11
|
getStudioSaveStatusCode,
|
|
8
12
|
retryStudioSave,
|
|
13
|
+
trackStudioEditBlocked,
|
|
9
14
|
} from "./studioSaveDiagnostics";
|
|
10
15
|
|
|
11
16
|
describe("studio save diagnostics", () => {
|
|
@@ -51,6 +56,30 @@ describe("studio save diagnostics", () => {
|
|
|
51
56
|
});
|
|
52
57
|
});
|
|
53
58
|
|
|
59
|
+
it("emits expected direct-edit refusals on edit_blocked", () => {
|
|
60
|
+
const error = new Error("This animation is computed at runtime");
|
|
61
|
+
|
|
62
|
+
trackStudioEditBlocked({
|
|
63
|
+
source: "gsap_commit",
|
|
64
|
+
error,
|
|
65
|
+
filePath: "index.html",
|
|
66
|
+
mutationType: "drag",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("edit_blocked", {
|
|
70
|
+
source: "gsap_commit",
|
|
71
|
+
error_message: error.message,
|
|
72
|
+
status_code: null,
|
|
73
|
+
file_path: "index.html",
|
|
74
|
+
mutation_type: "drag",
|
|
75
|
+
attempt: undefined,
|
|
76
|
+
label: undefined,
|
|
77
|
+
target_id: undefined,
|
|
78
|
+
target_selector: undefined,
|
|
79
|
+
target_source_file: undefined,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
54
83
|
it("reads nested status codes from error causes", () => {
|
|
55
84
|
const cause = new StudioSaveHttpError("Too many requests", 429);
|
|
56
85
|
const error = new Error("retry wrapper") as Error & { cause?: unknown };
|
|
@@ -172,6 +172,10 @@ export function trackStudioSaveFailure(input: StudioSaveFailureInput): void {
|
|
|
172
172
|
trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
export function trackStudioEditBlocked(input: StudioSaveFailureInput): void {
|
|
176
|
+
trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
|
|
177
|
+
}
|
|
178
|
+
|
|
175
179
|
export async function createStudioSaveHttpError(
|
|
176
180
|
response: Response,
|
|
177
181
|
fallbackMessage: string,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { useDomEditSelectionContext } from "../contexts/DomEditContext";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { useDomEditActionsContext, useDomEditSelectionContext } from "../contexts/DomEditContext";
|
|
3
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
4
4
|
import { usePlayerStore } from "../player";
|
|
5
|
-
import { useStudioAgentTools } from "./useStudioAgentTools";
|
|
5
|
+
import { useStudioAgentTools, type StudioAgentToolsDeps } from "./useStudioAgentTools";
|
|
6
6
|
import type { StudioLookSnapshot } from "./tools/lookTools";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -12,14 +12,32 @@ import type { StudioLookSnapshot } from "./tools/lookTools";
|
|
|
12
12
|
* contexts are only readable below `DomEditProvider`, which `App` renders, and
|
|
13
13
|
* `App.tsx` sits three lines under the 600-line cap.
|
|
14
14
|
*
|
|
15
|
-
* The player store is read IMPERATIVELY through `getState()`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* a value nothing here displays.
|
|
15
|
+
* The player store is read IMPERATIVELY through `getState()` rather than
|
|
16
|
+
* subscribed to. Subscribing to `currentTime` would re-render this component on
|
|
17
|
+
* every animation frame during playback for a value nothing here displays.
|
|
19
18
|
*/
|
|
20
19
|
export function StudioAgentTools() {
|
|
21
|
-
const { projectId, activeCompPath, editHistory } = useStudioShellContext();
|
|
22
|
-
const {
|
|
20
|
+
const { projectId, activeCompPath, editHistory, writeBlockedReason } = useStudioShellContext();
|
|
21
|
+
const {
|
|
22
|
+
domEditSelection,
|
|
23
|
+
selectedGsapAnimations,
|
|
24
|
+
gsapMultipleTimelines,
|
|
25
|
+
gsapUnsupportedTimelinePattern,
|
|
26
|
+
} = useDomEditSelectionContext();
|
|
27
|
+
const {
|
|
28
|
+
previewIframeRef,
|
|
29
|
+
buildDomSelectionFromTarget,
|
|
30
|
+
applyDomSelection,
|
|
31
|
+
handleDomTextCommit,
|
|
32
|
+
handleDomStyleCommit,
|
|
33
|
+
handleDomPathOffsetCommit,
|
|
34
|
+
handleDomBoxSizeCommit,
|
|
35
|
+
handleDomRotationCommit,
|
|
36
|
+
handleGsapAddAnimation,
|
|
37
|
+
handleGsapUpdateMeta,
|
|
38
|
+
handleGsapAddKeyframeBatch,
|
|
39
|
+
handleGsapDeleteAnimation,
|
|
40
|
+
} = useDomEditActionsContext();
|
|
23
41
|
|
|
24
42
|
const getSnapshot = useCallback((): StudioLookSnapshot => {
|
|
25
43
|
const player = usePlayerStore.getState();
|
|
@@ -41,6 +59,83 @@ export function StudioAgentTools() {
|
|
|
41
59
|
};
|
|
42
60
|
}, [projectId, activeCompPath, domEditSelection, selectedGsapAnimations, editHistory]);
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
const deps = useMemo<StudioAgentToolsDeps>(
|
|
63
|
+
() => ({
|
|
64
|
+
getSnapshot,
|
|
65
|
+
getPreviewDocument: () => previewIframeRef.current?.contentDocument ?? null,
|
|
66
|
+
buildSelection: (element) => buildDomSelectionFromTarget(element),
|
|
67
|
+
applySelection: (selection) => applyDomSelection(selection, { revealPanel: true }),
|
|
68
|
+
requestSeek: (time) => usePlayerStore.getState().requestSeek(time),
|
|
69
|
+
readPlayhead: () => {
|
|
70
|
+
const player = usePlayerStore.getState();
|
|
71
|
+
return {
|
|
72
|
+
currentTime: player.currentTime,
|
|
73
|
+
duration: player.duration,
|
|
74
|
+
isPlaying: player.isPlaying,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
getProjectId: () => projectId,
|
|
78
|
+
getCompositionPath: () => activeCompPath,
|
|
79
|
+
// HEAD, not GET: the tool only needs to know the frame renders. Pulling
|
|
80
|
+
// the PNG here would download it once for nothing, since the agent
|
|
81
|
+
// fetches the URL itself.
|
|
82
|
+
probeFrame: async (url) => {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
85
|
+
return { ok: response.ok, status: response.status };
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: false, status: 0 };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
91
|
+
getCurrentSelection: () => domEditSelection,
|
|
92
|
+
getWriteBlockedReason: () => writeBlockedReason,
|
|
93
|
+
setText: (value, fieldKey) => handleDomTextCommit(value, fieldKey),
|
|
94
|
+
setStyle: (property, value) => handleDomStyleCommit(property, value),
|
|
95
|
+
// Measured, not authored: the tool compares this before and after to
|
|
96
|
+
// tell a real change from a handler that did nothing and resolved.
|
|
97
|
+
readBox: (selection) => {
|
|
98
|
+
const rect = selection.element.getBoundingClientRect();
|
|
99
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
100
|
+
},
|
|
101
|
+
moveTo: (selection, next) => handleDomPathOffsetCommit(selection, next),
|
|
102
|
+
resizeTo: (selection, next) => handleDomBoxSizeCommit(selection, next),
|
|
103
|
+
rotateTo: (selection, next) => handleDomRotationCommit(selection, next),
|
|
104
|
+
addAnimation: (method) => handleGsapAddAnimation(method),
|
|
105
|
+
updateAnimation: (animationId, updates) => handleGsapUpdateMeta(animationId, updates),
|
|
106
|
+
addKeyframe: (animationId, percent, properties) =>
|
|
107
|
+
handleGsapAddKeyframeBatch(animationId, percent, properties),
|
|
108
|
+
deleteAnimation: (animationId) => handleGsapDeleteAnimation(animationId),
|
|
109
|
+
getGsapDiagnostics: () => ({
|
|
110
|
+
animations: selectedGsapAnimations,
|
|
111
|
+
multipleTimelines: gsapMultipleTimelines,
|
|
112
|
+
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
[
|
|
116
|
+
getSnapshot,
|
|
117
|
+
previewIframeRef,
|
|
118
|
+
buildDomSelectionFromTarget,
|
|
119
|
+
applyDomSelection,
|
|
120
|
+
projectId,
|
|
121
|
+
activeCompPath,
|
|
122
|
+
writeBlockedReason,
|
|
123
|
+
handleDomTextCommit,
|
|
124
|
+
handleDomStyleCommit,
|
|
125
|
+
handleDomPathOffsetCommit,
|
|
126
|
+
handleDomBoxSizeCommit,
|
|
127
|
+
handleDomRotationCommit,
|
|
128
|
+
handleGsapAddAnimation,
|
|
129
|
+
handleGsapUpdateMeta,
|
|
130
|
+
handleGsapAddKeyframeBatch,
|
|
131
|
+
handleGsapDeleteAnimation,
|
|
132
|
+
domEditSelection,
|
|
133
|
+
selectedGsapAnimations,
|
|
134
|
+
gsapMultipleTimelines,
|
|
135
|
+
gsapUnsupportedTimelinePattern,
|
|
136
|
+
],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
useStudioAgentTools(deps);
|
|
45
140
|
return null;
|
|
46
141
|
}
|
package/src/webmcp/toolResult.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function toolOk<T extends object>(value: T): { ok: true } & T {
|
|
|
36
36
|
return { ok: true, ...value };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function toolFailure(kind: ToolFailureKind, reason: string, hint?: string): ToolFailure {
|
|
39
|
+
export function toolFailure(kind: ToolFailureKind, reason: string, hint?: string): ToolFailure {
|
|
40
40
|
return hint ? { ok: false, kind, reason, hint } : { ok: false, kind, reason };
|
|
41
41
|
}
|
|
42
42
|
|