@hyperframes/studio 0.8.24 → 0.8.26
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-CTQCs_KG.js → hyperframes-player-C-CArn13.js} +1 -1
- package/dist/assets/{index-okdE01ky.js → index-BIXyJgK5.js} +1 -1
- package/dist/assets/{index-Bq3M0sjr.js → index-D5yni1t6.js} +218 -218
- package/dist/assets/index-DnRfAiK2.css +1 -0
- package/dist/assets/{index-9WJFcXTP.js → index-pfeP2Y63.js} +1 -1
- package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
- package/dist/chunk-A7S5SGNA.js.map +1 -0
- package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6230 -5019
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/editor/TopologyLens.test.tsx +294 -0
- package/src/components/editor/TopologyLens.tsx +206 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
- package/src/components/editor/domEditingLayers.test.ts +9 -0
- package/src/components/editor/domEditingLayers.ts +8 -8
- package/src/components/editor/topologyLensGeometry.test.ts +44 -0
- package/src/components/editor/topologyLensGeometry.ts +119 -0
- package/src/components/editor/topologyLensState.test.ts +116 -0
- package/src/components/editor/topologyLensState.ts +72 -0
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
- package/src/components/nle/PreviewOverlays.tsx +24 -18
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
- package/src/components/sidebar/CompositionsTab.tsx +10 -3
- package/src/components/ui/HyperframesLoader.tsx +3 -48
- package/src/components/ui/HyperframesMark.tsx +53 -0
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/hooks/domEditCommitRunner.test.ts +59 -0
- package/src/hooks/domEditCommitRunner.ts +55 -20
- package/src/hooks/domEditCommitTypes.ts +8 -1
- package/src/hooks/domEditTextCommitPlan.ts +47 -0
- package/src/hooks/timelineTrackVisibility.test.ts +7 -1
- package/src/hooks/timelineTrackVisibility.ts +5 -5
- package/src/hooks/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +243 -8
- package/src/hooks/useDomEditCommits.ts +69 -14
- package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
- package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
- package/src/hooks/useDomEditSession.ts +6 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
- package/src/hooks/useDomEditTextCommits.ts +64 -86
- package/src/hooks/useDomEditWiring.ts +1 -1
- package/src/hooks/useDomSelection.ts +2 -0
- package/src/hooks/useDomSelectionTypes.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
- package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
- package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
- package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
- package/src/hooks/useGsapAnimationOps.ts +4 -4
- package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
- package/src/hooks/useGsapAwareEditing.ts +31 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
- package/src/hooks/useGsapSelectionHandlers.ts +21 -11
- package/src/hooks/useRenderClipContent.test.ts +27 -0
- package/src/hooks/useRenderClipContent.ts +5 -0
- package/src/hooks/useStudioExternalFileChanges.ts +5 -0
- package/src/player/components/CompositionThumbnail.test.ts +66 -1
- package/src/player/components/CompositionThumbnail.tsx +6 -0
- package/src/player/hooks/timelineSyncHydration.ts +10 -2
- package/src/player/store/playerStore.test.ts +24 -0
- package/src/player/store/thumbnailSlice.ts +6 -0
- package/src/styles/studio.css +179 -0
- package/src/utils/domEditSaveQueue.test.ts +5 -3
- package/src/utils/domEditSaveQueue.ts +6 -1
- package/src/webmcp/StudioAgentTools.test.ts +70 -0
- package/src/webmcp/StudioAgentTools.tsx +89 -21
- package/src/webmcp/handles.test.ts +172 -15
- package/src/webmcp/handles.ts +152 -35
- package/src/webmcp/tools/animationTools.test.ts +241 -60
- package/src/webmcp/tools/animationTools.ts +191 -104
- package/src/webmcp/tools/contentTools.test.ts +103 -41
- package/src/webmcp/tools/contentTools.ts +149 -105
- package/src/webmcp/tools/inspectTools.test.ts +47 -5
- package/src/webmcp/tools/inspectTools.ts +59 -19
- package/src/webmcp/tools/lookTools.test.ts +185 -38
- package/src/webmcp/tools/lookTools.ts +134 -21
- package/src/webmcp/tools/selectionTools.test.ts +105 -9
- package/src/webmcp/tools/selectionTools.ts +34 -19
- package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
- package/src/webmcp/tools/transformTools.test.ts +140 -54
- package/src/webmcp/tools/transformTools.ts +215 -56
- package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
- package/src/webmcp/useStudioAgentTools.ts +27 -15
- package/src/webmcp/webmcpTestUtils.ts +37 -0
- package/src/webmcp/writeCoordinator.test.ts +375 -0
- package/src/webmcp/writeCoordinator.ts +473 -0
- package/dist/assets/index-yGhfxxoL.css +0 -1
- package/dist/chunk-6BT6DTB4.js.map +0 -1
- /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
|
@@ -3,9 +3,11 @@ import { act } from "react";
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { mountReactHarness } from "../hooks/domSelectionTestHarness";
|
|
5
5
|
import { writeStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
6
|
+
import { mintElementHandle } from "./handles";
|
|
6
7
|
import { useStudioAgentTools, type StudioAgentToolsDeps } from "./useStudioAgentTools";
|
|
7
8
|
import type { ModelContext, ModelContextRegisterToolOptions, ModelContextTool } from "./types";
|
|
8
9
|
import type { StudioLookSnapshot } from "./tools/lookTools";
|
|
10
|
+
import { previewDoc, selectionFor } from "./webmcpTestUtils";
|
|
9
11
|
|
|
10
12
|
const trackEvent = vi.hoisted(() => vi.fn());
|
|
11
13
|
vi.mock("../telemetry/client", () => ({ trackEvent }));
|
|
@@ -22,6 +24,7 @@ function snapshot(overrides: Partial<StudioLookSnapshot> = {}): StudioLookSnapsh
|
|
|
22
24
|
duration: 10,
|
|
23
25
|
isPlaying: false,
|
|
24
26
|
elements: [],
|
|
27
|
+
scene: { status: "ready", items: [], drillInItem: null },
|
|
25
28
|
selection: null,
|
|
26
29
|
selectionAnimationCount: 0,
|
|
27
30
|
history: { canUndo: false, canRedo: false, undoLabel: null, redoLabel: null },
|
|
@@ -50,10 +53,11 @@ function deps(overrides: Partial<StudioAgentToolsDeps> = {}): StudioAgentToolsDe
|
|
|
50
53
|
moveTo: async () => undefined,
|
|
51
54
|
resizeTo: async () => undefined,
|
|
52
55
|
rotateTo: async () => undefined,
|
|
53
|
-
addAnimation: () =>
|
|
56
|
+
addAnimation: async () => true,
|
|
54
57
|
updateAnimation: async () => true,
|
|
55
58
|
addKeyframe: async () => undefined,
|
|
56
|
-
deleteAnimation: () =>
|
|
59
|
+
deleteAnimation: async () => true,
|
|
60
|
+
getAnimationsForSelection: async () => [],
|
|
57
61
|
getGsapDiagnostics: () => ({
|
|
58
62
|
animations: [],
|
|
59
63
|
multipleTimelines: false,
|
|
@@ -63,6 +67,40 @@ function deps(overrides: Partial<StudioAgentToolsDeps> = {}): StudioAgentToolsDe
|
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
async function executeRegistered<T>(
|
|
71
|
+
registered: ModelContextTool[],
|
|
72
|
+
name: string,
|
|
73
|
+
input: object,
|
|
74
|
+
signal: AbortSignal = new AbortController().signal,
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const tool = registered.find((candidate) => candidate.name === name);
|
|
77
|
+
if (!tool) throw new Error(`expected ${name} to be registered`);
|
|
78
|
+
return (await tool.execute(input, { signal })) as T;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mountedTargetDeps(overrides: Partial<StudioAgentToolsDeps> = {}) {
|
|
82
|
+
const doc = previewDoc('<h1 id="human">Human</h1><h1 id="agent">Agent</h1>');
|
|
83
|
+
const human = doc.getElementById("human") as HTMLElement;
|
|
84
|
+
const agent = doc.getElementById("agent") as HTMLElement;
|
|
85
|
+
const agentHandle = mintElementHandle({
|
|
86
|
+
projectId: "demo",
|
|
87
|
+
domId: "agent",
|
|
88
|
+
sourceFile: "index.html",
|
|
89
|
+
activeCompositionPath: "index.html",
|
|
90
|
+
});
|
|
91
|
+
if (!agentHandle) throw new Error("expected agent handle");
|
|
92
|
+
return {
|
|
93
|
+
agent,
|
|
94
|
+
agentHandle,
|
|
95
|
+
currentSelection: selectionFor(human),
|
|
96
|
+
deps: deps({
|
|
97
|
+
getPreviewDocument: () => doc,
|
|
98
|
+
buildSelection: async (element) => selectionFor(element),
|
|
99
|
+
...overrides,
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
/** Install a fake `document.modelContext` and report what got registered. */
|
|
67
105
|
function installModelContext() {
|
|
68
106
|
const registered: ModelContextTool[] = [];
|
|
@@ -80,6 +118,12 @@ function installModelContext() {
|
|
|
80
118
|
return { registered, registerTool };
|
|
81
119
|
}
|
|
82
120
|
|
|
121
|
+
async function executeFirstRegistered<T>(registered: ModelContextTool[]): Promise<T> {
|
|
122
|
+
const look = registered[0];
|
|
123
|
+
if (!look) throw new Error("expected studio_look to be registered");
|
|
124
|
+
return (await look.execute({}, { signal: new AbortController().signal })) as T;
|
|
125
|
+
}
|
|
126
|
+
|
|
83
127
|
function removeModelContext() {
|
|
84
128
|
Reflect.deleteProperty(document, "modelContext");
|
|
85
129
|
}
|
|
@@ -170,12 +214,10 @@ describe("useStudioAgentTools", () => {
|
|
|
170
214
|
harness?.rerenderWith(deps({ getSnapshot: () => snapshot({ currentTime: 42 }) }));
|
|
171
215
|
});
|
|
172
216
|
|
|
173
|
-
const
|
|
174
|
-
if (!look) throw new Error("expected studio_look to be registered");
|
|
175
|
-
const result = (await look.execute({}, { signal: new AbortController().signal })) as {
|
|
217
|
+
const result = await executeFirstRegistered<{
|
|
176
218
|
ok: boolean;
|
|
177
219
|
playhead: number;
|
|
178
|
-
};
|
|
220
|
+
}>(registered);
|
|
179
221
|
|
|
180
222
|
expect(result.ok).toBe(true);
|
|
181
223
|
expect(result.playhead).toBe(42);
|
|
@@ -257,14 +299,272 @@ describe("useStudioAgentTools", () => {
|
|
|
257
299
|
});
|
|
258
300
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
259
301
|
|
|
260
|
-
const
|
|
261
|
-
if (!look) throw new Error("expected studio_look to be registered");
|
|
262
|
-
const result = (await look.execute({}, { signal: new AbortController().signal })) as {
|
|
302
|
+
const result = await executeFirstRegistered<{
|
|
263
303
|
ok: boolean;
|
|
264
304
|
kind: string;
|
|
265
|
-
};
|
|
305
|
+
}>(registered);
|
|
266
306
|
|
|
267
307
|
expect(result.ok).toBe(false);
|
|
268
308
|
expect(result.kind).toBe("internal");
|
|
269
309
|
});
|
|
310
|
+
|
|
311
|
+
it("requires a source-safe handle on every source-writing tool", async () => {
|
|
312
|
+
const { registered } = installModelContext();
|
|
313
|
+
await act(async () => mountTools(deps()));
|
|
314
|
+
|
|
315
|
+
for (const name of [
|
|
316
|
+
"studio_set_text",
|
|
317
|
+
"studio_set_style",
|
|
318
|
+
"studio_transform",
|
|
319
|
+
"studio_add_animation",
|
|
320
|
+
"studio_update_animation",
|
|
321
|
+
"studio_add_keyframe",
|
|
322
|
+
"studio_delete_animation",
|
|
323
|
+
]) {
|
|
324
|
+
const schema = registered.find((tool) => tool.name === name)?.inputSchema as {
|
|
325
|
+
required?: string[];
|
|
326
|
+
};
|
|
327
|
+
expect(schema.required, name).toContain("handle");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("binds a text write to the explicit handle even when human selection points elsewhere", async () => {
|
|
332
|
+
const { registered } = installModelContext();
|
|
333
|
+
const targeted = mountedTargetDeps();
|
|
334
|
+
const setText = vi.fn(
|
|
335
|
+
async () =>
|
|
336
|
+
({
|
|
337
|
+
ok: true,
|
|
338
|
+
persistence: {
|
|
339
|
+
sourceFile: "index.html",
|
|
340
|
+
version: '"sha256:after"',
|
|
341
|
+
changed: true,
|
|
342
|
+
},
|
|
343
|
+
}) as const,
|
|
344
|
+
);
|
|
345
|
+
await act(async () => mountTools({ ...targeted.deps, setText }));
|
|
346
|
+
|
|
347
|
+
const result = await executeRegistered<{
|
|
348
|
+
ok: boolean;
|
|
349
|
+
stage: string;
|
|
350
|
+
changed: boolean;
|
|
351
|
+
target: { handle: string; sourceFile: string };
|
|
352
|
+
}>(registered, "studio_set_text", {
|
|
353
|
+
handle: targeted.agentHandle,
|
|
354
|
+
text: "Edited by agent",
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(setText).toHaveBeenCalledWith(
|
|
358
|
+
expect.objectContaining({ element: targeted.agent }),
|
|
359
|
+
"Edited by agent",
|
|
360
|
+
"self",
|
|
361
|
+
);
|
|
362
|
+
expect(result).toMatchObject({
|
|
363
|
+
ok: true,
|
|
364
|
+
stage: "saved",
|
|
365
|
+
changed: true,
|
|
366
|
+
target: { handle: targeted.agentHandle, sourceFile: "index.html" },
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("refuses a locked target and an early abort before invoking the write actor", async () => {
|
|
371
|
+
const { registered } = installModelContext();
|
|
372
|
+
const setText = vi.fn();
|
|
373
|
+
const targeted = mountedTargetDeps({
|
|
374
|
+
buildSelection: async (element) => selectionFor(element, { isInsideLockedComposition: true }),
|
|
375
|
+
});
|
|
376
|
+
await act(async () => mountTools({ ...targeted.deps, setText }));
|
|
377
|
+
|
|
378
|
+
const locked = await executeRegistered<{ ok: boolean; stage: string }>(
|
|
379
|
+
registered,
|
|
380
|
+
"studio_set_text",
|
|
381
|
+
{ handle: targeted.agentHandle, text: "No" },
|
|
382
|
+
);
|
|
383
|
+
const controller = new AbortController();
|
|
384
|
+
controller.abort();
|
|
385
|
+
const aborted = await executeRegistered<{ ok: boolean; stage: string; cancelRequested: true }>(
|
|
386
|
+
registered,
|
|
387
|
+
"studio_set_text",
|
|
388
|
+
{ handle: targeted.agentHandle, text: "No" },
|
|
389
|
+
controller.signal,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
expect(locked).toMatchObject({ ok: false, stage: "refused" });
|
|
393
|
+
expect(aborted).toMatchObject({ ok: false, stage: "refused", cancelRequested: true });
|
|
394
|
+
expect(setText).not.toHaveBeenCalled();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("reports dispatched, saved, and verified only from their corresponding evidence", async () => {
|
|
398
|
+
const { registered } = installModelContext();
|
|
399
|
+
const targeted = mountedTargetDeps();
|
|
400
|
+
const box = { x: 0, y: 0, width: 100, height: 50 };
|
|
401
|
+
await act(async () =>
|
|
402
|
+
mountTools({
|
|
403
|
+
...targeted.deps,
|
|
404
|
+
setText: async () => ({
|
|
405
|
+
ok: true,
|
|
406
|
+
persistence: {
|
|
407
|
+
sourceFile: "index.html",
|
|
408
|
+
version: '"sha256:after"',
|
|
409
|
+
changed: true,
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
addAnimation: async () => true,
|
|
413
|
+
readBox: () => ({ ...box }),
|
|
414
|
+
resizeTo: async (_selection, next) => {
|
|
415
|
+
Object.assign(box, next);
|
|
416
|
+
return {
|
|
417
|
+
ok: true,
|
|
418
|
+
persistence: {
|
|
419
|
+
sourceFile: "index.html",
|
|
420
|
+
version: '"sha256:transform"',
|
|
421
|
+
changed: true,
|
|
422
|
+
},
|
|
423
|
+
} as const;
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const dispatched = await executeRegistered<{ stage: string }>(
|
|
429
|
+
registered,
|
|
430
|
+
"studio_add_animation",
|
|
431
|
+
{ handle: targeted.agentHandle, method: "to" },
|
|
432
|
+
);
|
|
433
|
+
const saved = await executeRegistered<{ stage: string }>(registered, "studio_set_text", {
|
|
434
|
+
handle: targeted.agentHandle,
|
|
435
|
+
text: "Saved",
|
|
436
|
+
});
|
|
437
|
+
const verified = await executeRegistered<{ stage: string }>(registered, "studio_transform", {
|
|
438
|
+
handle: targeted.agentHandle,
|
|
439
|
+
width: 200,
|
|
440
|
+
height: 80,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(dispatched.stage).toBe("dispatched");
|
|
444
|
+
expect(saved.stage).toBe("saved");
|
|
445
|
+
expect(verified.stage).toBe("verified");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("reports a matched no-op with its durable version without fabricating a change", async () => {
|
|
449
|
+
const { registered } = installModelContext();
|
|
450
|
+
const targeted = mountedTargetDeps();
|
|
451
|
+
await act(async () =>
|
|
452
|
+
mountTools({
|
|
453
|
+
...targeted.deps,
|
|
454
|
+
setText: async () => ({
|
|
455
|
+
ok: true,
|
|
456
|
+
persistence: {
|
|
457
|
+
sourceFile: "index.html",
|
|
458
|
+
version: '"sha256:current"',
|
|
459
|
+
changed: false,
|
|
460
|
+
},
|
|
461
|
+
}),
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const result = await executeRegistered<{
|
|
466
|
+
ok: boolean;
|
|
467
|
+
stage: string;
|
|
468
|
+
changed: boolean;
|
|
469
|
+
evidence: { version: string };
|
|
470
|
+
}>(registered, "studio_set_text", {
|
|
471
|
+
handle: targeted.agentHandle,
|
|
472
|
+
text: "Agent",
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(result).toMatchObject({
|
|
476
|
+
ok: true,
|
|
477
|
+
stage: "saved",
|
|
478
|
+
changed: false,
|
|
479
|
+
evidence: { version: '"sha256:current"' },
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("reports the actual saved outcome and late cancellation without promising rollback", async () => {
|
|
484
|
+
const { registered } = installModelContext();
|
|
485
|
+
const targeted = mountedTargetDeps();
|
|
486
|
+
let finish: (() => void) | undefined;
|
|
487
|
+
const setText = vi.fn(
|
|
488
|
+
() =>
|
|
489
|
+
new Promise<{
|
|
490
|
+
ok: true;
|
|
491
|
+
persistence: { sourceFile: string; version: string; changed: true };
|
|
492
|
+
}>((resolve) => {
|
|
493
|
+
finish = () =>
|
|
494
|
+
resolve({
|
|
495
|
+
ok: true,
|
|
496
|
+
persistence: {
|
|
497
|
+
sourceFile: "index.html",
|
|
498
|
+
version: '"sha256:late"',
|
|
499
|
+
changed: true,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
await act(async () => mountTools({ ...targeted.deps, setText }));
|
|
505
|
+
const controller = new AbortController();
|
|
506
|
+
|
|
507
|
+
const pending = executeRegistered<{
|
|
508
|
+
ok: boolean;
|
|
509
|
+
stage: string;
|
|
510
|
+
cancelRequested?: boolean;
|
|
511
|
+
}>(
|
|
512
|
+
registered,
|
|
513
|
+
"studio_set_text",
|
|
514
|
+
{ handle: targeted.agentHandle, text: "Late" },
|
|
515
|
+
controller.signal,
|
|
516
|
+
);
|
|
517
|
+
await vi.waitFor(() => expect(setText).toHaveBeenCalledTimes(1));
|
|
518
|
+
controller.abort();
|
|
519
|
+
finish?.();
|
|
520
|
+
|
|
521
|
+
expect(await pending).toMatchObject({
|
|
522
|
+
ok: true,
|
|
523
|
+
stage: "saved",
|
|
524
|
+
cancelRequested: true,
|
|
525
|
+
});
|
|
526
|
+
expect(setText).toHaveBeenCalledTimes(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("aggregates partial style evidence and uses the weakest applied assurance", async () => {
|
|
530
|
+
const { registered } = installModelContext();
|
|
531
|
+
const targeted = mountedTargetDeps();
|
|
532
|
+
const setStyle = vi.fn(async (_selection, property: string) => {
|
|
533
|
+
if (property === "left") return { ok: false, reason: "geometry-property" } as const;
|
|
534
|
+
if (property === "opacity") return { ok: true } as const;
|
|
535
|
+
return {
|
|
536
|
+
ok: true,
|
|
537
|
+
persistence: {
|
|
538
|
+
sourceFile: "index.html",
|
|
539
|
+
version: '"sha256:color"',
|
|
540
|
+
changed: true,
|
|
541
|
+
},
|
|
542
|
+
} as const;
|
|
543
|
+
});
|
|
544
|
+
await act(async () => mountTools({ ...targeted.deps, setStyle }));
|
|
545
|
+
|
|
546
|
+
const result = await executeRegistered<{
|
|
547
|
+
ok: boolean;
|
|
548
|
+
stage: string;
|
|
549
|
+
partial: boolean;
|
|
550
|
+
applied: Record<string, string>;
|
|
551
|
+
rejected: Record<string, string>;
|
|
552
|
+
propertyReceipts: Record<string, { stage: string }>;
|
|
553
|
+
}>(registered, "studio_set_style", {
|
|
554
|
+
handle: targeted.agentHandle,
|
|
555
|
+
styles: { color: "red", left: "10px", opacity: "0.5" },
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(result).toMatchObject({
|
|
559
|
+
ok: true,
|
|
560
|
+
stage: "dispatched",
|
|
561
|
+
partial: true,
|
|
562
|
+
applied: { color: "red", opacity: "0.5" },
|
|
563
|
+
rejected: { left: "geometry-property" },
|
|
564
|
+
propertyReceipts: {
|
|
565
|
+
color: { stage: "saved" },
|
|
566
|
+
opacity: { stage: "dispatched" },
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
});
|
|
270
570
|
});
|
|
@@ -177,8 +177,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
177
177
|
description: STUDIO_SET_TEXT_DESCRIPTION,
|
|
178
178
|
inputSchema: STUDIO_SET_TEXT_INPUT_SCHEMA,
|
|
179
179
|
annotations: { readOnlyHint: false, untrustedContentHint: true },
|
|
180
|
-
execute: (input): Promise<ToolResult<StudioSetTextResult>> =>
|
|
181
|
-
runToolBody("studio_set_text", () =>
|
|
180
|
+
execute: (input, { signal }): Promise<ToolResult<StudioSetTextResult>> =>
|
|
181
|
+
runToolBody<StudioSetTextResult>("studio_set_text", () =>
|
|
182
|
+
studioSetText(depsRef.current, input, signal),
|
|
183
|
+
),
|
|
182
184
|
},
|
|
183
185
|
{
|
|
184
186
|
name: "studio_set_style",
|
|
@@ -186,8 +188,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
186
188
|
description: STUDIO_SET_STYLE_DESCRIPTION,
|
|
187
189
|
inputSchema: STUDIO_SET_STYLE_INPUT_SCHEMA,
|
|
188
190
|
annotations: { readOnlyHint: false },
|
|
189
|
-
execute: (input): Promise<ToolResult<StudioSetStyleResult>> =>
|
|
190
|
-
runToolBody("studio_set_style", () =>
|
|
191
|
+
execute: (input, { signal }): Promise<ToolResult<StudioSetStyleResult>> =>
|
|
192
|
+
runToolBody<StudioSetStyleResult>("studio_set_style", () =>
|
|
193
|
+
studioSetStyle(depsRef.current, input, signal),
|
|
194
|
+
),
|
|
191
195
|
},
|
|
192
196
|
{
|
|
193
197
|
name: "studio_transform",
|
|
@@ -195,9 +199,9 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
195
199
|
description: STUDIO_TRANSFORM_DESCRIPTION,
|
|
196
200
|
inputSchema: STUDIO_TRANSFORM_INPUT_SCHEMA,
|
|
197
201
|
annotations: { readOnlyHint: false },
|
|
198
|
-
execute: (input): Promise<ToolResult<StudioTransformResult>> =>
|
|
199
|
-
runToolBody("studio_transform", () =>
|
|
200
|
-
studioTransform(depsRef.current, input as StudioTransformInput),
|
|
202
|
+
execute: (input, { signal }): Promise<ToolResult<StudioTransformResult>> =>
|
|
203
|
+
runToolBody<StudioTransformResult>("studio_transform", () =>
|
|
204
|
+
studioTransform(depsRef.current, input as StudioTransformInput, signal),
|
|
201
205
|
),
|
|
202
206
|
},
|
|
203
207
|
{
|
|
@@ -206,8 +210,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
206
210
|
description: STUDIO_ADD_ANIMATION_DESCRIPTION,
|
|
207
211
|
inputSchema: STUDIO_ADD_ANIMATION_INPUT_SCHEMA,
|
|
208
212
|
annotations: { readOnlyHint: false },
|
|
209
|
-
execute: (input): Promise<ToolResult<StudioAddAnimationResult>> =>
|
|
210
|
-
runToolBody("studio_add_animation", () =>
|
|
213
|
+
execute: (input, { signal }): Promise<ToolResult<StudioAddAnimationResult>> =>
|
|
214
|
+
runToolBody<StudioAddAnimationResult>("studio_add_animation", () =>
|
|
215
|
+
studioAddAnimation(depsRef.current, input, signal),
|
|
216
|
+
),
|
|
211
217
|
},
|
|
212
218
|
{
|
|
213
219
|
name: "studio_update_animation",
|
|
@@ -215,8 +221,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
215
221
|
description: STUDIO_UPDATE_ANIMATION_DESCRIPTION,
|
|
216
222
|
inputSchema: STUDIO_UPDATE_ANIMATION_INPUT_SCHEMA,
|
|
217
223
|
annotations: { readOnlyHint: false },
|
|
218
|
-
execute: (input): Promise<ToolResult<StudioUpdateAnimationResult>> =>
|
|
219
|
-
runToolBody("studio_update_animation", () =>
|
|
224
|
+
execute: (input, { signal }): Promise<ToolResult<StudioUpdateAnimationResult>> =>
|
|
225
|
+
runToolBody<StudioUpdateAnimationResult>("studio_update_animation", () =>
|
|
226
|
+
studioUpdateAnimation(depsRef.current, input, signal),
|
|
227
|
+
),
|
|
220
228
|
},
|
|
221
229
|
{
|
|
222
230
|
name: "studio_add_keyframe",
|
|
@@ -224,8 +232,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
224
232
|
description: STUDIO_ADD_KEYFRAME_DESCRIPTION,
|
|
225
233
|
inputSchema: STUDIO_ADD_KEYFRAME_INPUT_SCHEMA,
|
|
226
234
|
annotations: { readOnlyHint: false },
|
|
227
|
-
execute: (input): Promise<ToolResult<StudioAddKeyframeResult>> =>
|
|
228
|
-
runToolBody("studio_add_keyframe", () =>
|
|
235
|
+
execute: (input, { signal }): Promise<ToolResult<StudioAddKeyframeResult>> =>
|
|
236
|
+
runToolBody<StudioAddKeyframeResult>("studio_add_keyframe", () =>
|
|
237
|
+
studioAddKeyframe(depsRef.current, input, signal),
|
|
238
|
+
),
|
|
229
239
|
},
|
|
230
240
|
{
|
|
231
241
|
name: "studio_delete_animation",
|
|
@@ -233,8 +243,10 @@ function buildStudioTools(depsRef: { readonly current: StudioAgentToolsDeps }):
|
|
|
233
243
|
description: STUDIO_DELETE_ANIMATION_DESCRIPTION,
|
|
234
244
|
inputSchema: STUDIO_DELETE_ANIMATION_INPUT_SCHEMA,
|
|
235
245
|
annotations: { readOnlyHint: false },
|
|
236
|
-
execute: (input): Promise<ToolResult<StudioDeleteAnimationResult>> =>
|
|
237
|
-
runToolBody("studio_delete_animation", () =>
|
|
246
|
+
execute: (input, { signal }): Promise<ToolResult<StudioDeleteAnimationResult>> =>
|
|
247
|
+
runToolBody<StudioDeleteAnimationResult>("studio_delete_animation", () =>
|
|
248
|
+
studioDeleteAnimation(depsRef.current, input, signal),
|
|
249
|
+
),
|
|
238
250
|
},
|
|
239
251
|
];
|
|
240
252
|
}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import { expect } from "vitest";
|
|
9
9
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
10
10
|
import type { ToolFailure, ToolResult } from "./toolResult";
|
|
11
|
+
import type { SelectionToolDeps } from "./tools/selectionTools";
|
|
12
|
+
import { mintElementHandle } from "./handles";
|
|
13
|
+
import type { TargetedWriteDeps } from "./writeCoordinator";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* An element inside a real iframe, which is where Studio's chrome expects to
|
|
@@ -78,6 +81,40 @@ export function selectionFor(
|
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
export function selectionToolDeps(overrides: Partial<SelectionToolDeps> = {}): SelectionToolDeps {
|
|
85
|
+
return {
|
|
86
|
+
getPreviewDocument: () => null,
|
|
87
|
+
getCompositionPath: () => "index.html",
|
|
88
|
+
getProjectId: () => "project-a",
|
|
89
|
+
buildSelection: async (element) => selectionFor(element),
|
|
90
|
+
applySelection: () => undefined,
|
|
91
|
+
requestSeek: () => undefined,
|
|
92
|
+
readPlayhead: () => ({ currentTime: 0, duration: 10, isPlaying: false }),
|
|
93
|
+
...overrides,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function sourceHandle(domId: string, projectId = "project-a"): string {
|
|
98
|
+
const handle = mintElementHandle({
|
|
99
|
+
projectId,
|
|
100
|
+
domId,
|
|
101
|
+
sourceFile: "index.html",
|
|
102
|
+
activeCompositionPath: "index.html",
|
|
103
|
+
});
|
|
104
|
+
if (!handle) throw new Error(`expected source handle for #${domId}`);
|
|
105
|
+
return handle;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function targetedWriteDeps(selection: DomEditSelection): TargetedWriteDeps {
|
|
109
|
+
return {
|
|
110
|
+
getPreviewDocument: () => selection.element.ownerDocument,
|
|
111
|
+
getProjectId: () => "project-a",
|
|
112
|
+
getWriteBlockedReason: () => null,
|
|
113
|
+
buildSelection: async () => selection,
|
|
114
|
+
applySelection: () => undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
export function expectOk<T>(result: ToolResult<T>): { ok: true } & T {
|
|
82
119
|
expect(result.ok, `expected ok, got ${JSON.stringify(result)}`).toBe(true);
|
|
83
120
|
if (!result.ok) throw new Error("unreachable");
|