@hyperframes/studio 0.8.24 → 0.8.25
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-CTP_00ix.js} +1 -1
- package/dist/assets/{index-okdE01ky.js → index-Bjb4YqS_.js} +1 -1
- package/dist/assets/{index-Bq3M0sjr.js → index-DSk_ALFr.js} +218 -218
- package/dist/assets/{index-9WJFcXTP.js → index-Di-8Jxqc.js} +1 -1
- package/dist/assets/index-DnRfAiK2.css +1 -0
- 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 +6206 -5011
- 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/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +207 -8
- package/src/hooks/useDomEditCommits.ts +54 -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/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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
2
|
+
import { parseElementHandle, resolveLiveHandleSelection } from "./handles";
|
|
3
|
+
import { toolFailure, type ToolFailure, type ToolFailureKind } from "./toolResult";
|
|
4
|
+
|
|
5
|
+
export type StudioWriteOperation =
|
|
6
|
+
| "set-text"
|
|
7
|
+
| "set-style"
|
|
8
|
+
| "transform"
|
|
9
|
+
| "add-animation"
|
|
10
|
+
| "update-animation"
|
|
11
|
+
| "add-keyframe"
|
|
12
|
+
| "delete-animation";
|
|
13
|
+
|
|
14
|
+
export const WRITE_RECEIPT_DESCRIPTION =
|
|
15
|
+
"Read `stage` as the proof level: refused, dispatched, saved, verified, or failed. " +
|
|
16
|
+
"`changed` is separate, and evidence explains what was actually proven.";
|
|
17
|
+
|
|
18
|
+
export interface StudioWriteTarget {
|
|
19
|
+
handle: string;
|
|
20
|
+
sourceFile: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StudioWriteEvidence =
|
|
24
|
+
| { kind: "dispatch"; followUp: "studio_inspect" }
|
|
25
|
+
| { kind: "content-version"; sourceFile: string; version: string }
|
|
26
|
+
| {
|
|
27
|
+
kind: "readback";
|
|
28
|
+
sourceFile: string;
|
|
29
|
+
version: string;
|
|
30
|
+
before: unknown;
|
|
31
|
+
after: unknown;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StudioWriteAdapterSuccess<T extends object> = T & {
|
|
35
|
+
ok: true;
|
|
36
|
+
stage: "dispatched" | "saved" | "verified";
|
|
37
|
+
changed: boolean;
|
|
38
|
+
evidence: StudioWriteEvidence;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type StudioWriteAdapterResult<T extends object> = StudioWriteAdapterSuccess<T> | ToolFailure;
|
|
42
|
+
|
|
43
|
+
export type StudioWriteResult<T extends object> =
|
|
44
|
+
| (T & {
|
|
45
|
+
ok: true;
|
|
46
|
+
stage: "dispatched" | "saved" | "verified";
|
|
47
|
+
target: StudioWriteTarget;
|
|
48
|
+
operation: StudioWriteOperation;
|
|
49
|
+
changed: boolean;
|
|
50
|
+
evidence: StudioWriteEvidence;
|
|
51
|
+
cancelRequested?: true;
|
|
52
|
+
})
|
|
53
|
+
| (ToolFailure & {
|
|
54
|
+
stage: "refused" | "failed";
|
|
55
|
+
target?: StudioWriteTarget;
|
|
56
|
+
operation: StudioWriteOperation;
|
|
57
|
+
cancelRequested?: true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export interface TargetedWriteDeps {
|
|
61
|
+
getPreviewDocument: () => Document | null;
|
|
62
|
+
getProjectId: () => string | null;
|
|
63
|
+
getWriteBlockedReason: () => string | null;
|
|
64
|
+
buildSelection: (element: HTMLElement) => Promise<DomEditSelection | null>;
|
|
65
|
+
applySelection: (selection: DomEditSelection) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type StudioEditLifecycleState =
|
|
69
|
+
| { phase: "idle" }
|
|
70
|
+
| {
|
|
71
|
+
callId: string;
|
|
72
|
+
projectId: string;
|
|
73
|
+
target: StudioWriteTarget;
|
|
74
|
+
operation: StudioWriteOperation;
|
|
75
|
+
targetChanged: boolean;
|
|
76
|
+
phase: "dispatching" | "dispatched" | "saved" | "verified" | "failed";
|
|
77
|
+
receipt?: StudioWriteResult<object>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type LifecycleListener = () => void;
|
|
81
|
+
|
|
82
|
+
class StudioEditLifecycle {
|
|
83
|
+
private state: StudioEditLifecycleState = { phase: "idle" };
|
|
84
|
+
private listeners = new Set<LifecycleListener>();
|
|
85
|
+
private lastTerminalTarget: { projectId: string; handle: string } | null = null;
|
|
86
|
+
private activeProjectId: string | null = null;
|
|
87
|
+
private callSequence = 0;
|
|
88
|
+
|
|
89
|
+
getSnapshot = (): StudioEditLifecycleState => this.state;
|
|
90
|
+
|
|
91
|
+
subscribe = (listener: LifecycleListener): (() => void) => {
|
|
92
|
+
this.listeners.add(listener);
|
|
93
|
+
return () => this.listeners.delete(listener);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
begin(projectId: string, target: StudioWriteTarget, operation: StudioWriteOperation): string {
|
|
97
|
+
this.activateProject(projectId);
|
|
98
|
+
const callId = `studio-edit-${++this.callSequence}`;
|
|
99
|
+
const previous = this.lastTerminalTarget;
|
|
100
|
+
const targetChanged =
|
|
101
|
+
previous === null || previous.projectId !== projectId || previous.handle !== target.handle;
|
|
102
|
+
this.publish({
|
|
103
|
+
callId,
|
|
104
|
+
projectId,
|
|
105
|
+
target,
|
|
106
|
+
operation,
|
|
107
|
+
targetChanged,
|
|
108
|
+
phase: "dispatching",
|
|
109
|
+
});
|
|
110
|
+
return callId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
finish(callId: string, receipt: StudioWriteResult<object>): void {
|
|
114
|
+
if (this.state.phase === "idle" || this.state.callId !== callId) return;
|
|
115
|
+
this.lastTerminalTarget = {
|
|
116
|
+
projectId: this.state.projectId,
|
|
117
|
+
handle: this.state.target.handle,
|
|
118
|
+
};
|
|
119
|
+
this.publish({
|
|
120
|
+
...this.state,
|
|
121
|
+
phase: receipt.ok ? receipt.stage : "failed",
|
|
122
|
+
receipt,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Clear only the invocation that still owns the visual lifecycle. */
|
|
127
|
+
dismiss(callId: string): void {
|
|
128
|
+
if (this.state.phase === "idle" || this.state.callId !== callId) return;
|
|
129
|
+
this.publish({ phase: "idle" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
reset(): void {
|
|
133
|
+
this.activeProjectId = null;
|
|
134
|
+
this.lastTerminalTarget = null;
|
|
135
|
+
this.publish({ phase: "idle" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
activateProject(projectId: string | null): void {
|
|
139
|
+
if (this.activeProjectId === projectId) return;
|
|
140
|
+
this.activeProjectId = projectId;
|
|
141
|
+
this.lastTerminalTarget = null;
|
|
142
|
+
this.publish({ phase: "idle" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private publish(state: StudioEditLifecycleState): void {
|
|
146
|
+
this.state = state;
|
|
147
|
+
for (const listener of this.listeners) listener();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One state owner shared by the coordinator and Studio's future overlay. */
|
|
152
|
+
export const studioEditLifecycle = new StudioEditLifecycle();
|
|
153
|
+
|
|
154
|
+
interface TargetedWriteInput<T extends object> {
|
|
155
|
+
handle: unknown;
|
|
156
|
+
operation: StudioWriteOperation;
|
|
157
|
+
signal: AbortSignal;
|
|
158
|
+
preflight?: (selection: DomEditSelection) => ToolFailure | null | Promise<ToolFailure | null>;
|
|
159
|
+
write: (selection: DomEditSelection) => Promise<StudioWriteAdapterResult<T>>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type ResolvedWriteSelection =
|
|
163
|
+
| { ok: true; selection: DomEditSelection }
|
|
164
|
+
| { ok: false; failure: ToolFailure };
|
|
165
|
+
|
|
166
|
+
async function resolveWriteSelection(
|
|
167
|
+
deps: TargetedWriteDeps,
|
|
168
|
+
handle: string,
|
|
169
|
+
): Promise<ResolvedWriteSelection> {
|
|
170
|
+
const resolved = await resolveLiveHandleSelection(
|
|
171
|
+
deps.getPreviewDocument,
|
|
172
|
+
handle,
|
|
173
|
+
deps.buildSelection,
|
|
174
|
+
);
|
|
175
|
+
if (resolved.status === "preview-unavailable") {
|
|
176
|
+
return { ok: false, failure: toolFailure("blocked", "the composition preview is not ready") };
|
|
177
|
+
}
|
|
178
|
+
if (resolved.status === "not-found") {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
failure: toolFailure("invalid", "the target handle is stale", "Call studio_look and retry."),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (resolved.status === "unsupported") {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
failure: toolFailure(
|
|
188
|
+
"blocked",
|
|
189
|
+
"the target resolved to an element Studio cannot edit",
|
|
190
|
+
"Try a parent or child element from studio_look.",
|
|
191
|
+
),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (resolved.status === "changed") {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
failure: toolFailure(
|
|
198
|
+
"invalid",
|
|
199
|
+
"the target changed while it was resolving",
|
|
200
|
+
"Call studio_look again.",
|
|
201
|
+
),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, selection: resolved.selection };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function refusedResult<T extends object>(
|
|
208
|
+
operation: StudioWriteOperation,
|
|
209
|
+
failure: ToolFailure,
|
|
210
|
+
cancelRequested = false,
|
|
211
|
+
): StudioWriteResult<T> {
|
|
212
|
+
return {
|
|
213
|
+
...failure,
|
|
214
|
+
stage: "refused",
|
|
215
|
+
operation,
|
|
216
|
+
...(cancelRequested ? { cancelRequested: true as const } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function completedWrite<T extends object>(
|
|
221
|
+
adapter: StudioWriteAdapterResult<T>,
|
|
222
|
+
target: StudioWriteTarget,
|
|
223
|
+
operation: StudioWriteOperation,
|
|
224
|
+
cancelRequested: boolean,
|
|
225
|
+
): StudioWriteResult<T> {
|
|
226
|
+
const cancellation = cancelRequested ? { cancelRequested: true as const } : {};
|
|
227
|
+
return adapter.ok
|
|
228
|
+
? { ...adapter, target, operation, ...cancellation }
|
|
229
|
+
: { ...asFailed(adapter), stage: "failed", target, operation, ...cancellation };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function classifyThrownError(error: unknown): { kind: ToolFailureKind; reason: string } {
|
|
233
|
+
return {
|
|
234
|
+
kind: error instanceof TypeError || error instanceof ReferenceError ? "internal" : "failed",
|
|
235
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type ValidatedWriteAddress<T extends object> =
|
|
240
|
+
| { ok: true; handle: string; projectId: string }
|
|
241
|
+
| { ok: false; result: StudioWriteResult<T> };
|
|
242
|
+
|
|
243
|
+
function validateProjectScopedHandle<T extends object>(
|
|
244
|
+
handle: string,
|
|
245
|
+
projectId: string,
|
|
246
|
+
operation: StudioWriteOperation,
|
|
247
|
+
): StudioWriteResult<T> | null {
|
|
248
|
+
const parsedHandle = parseElementHandle(handle);
|
|
249
|
+
if (!parsedHandle || parsedHandle.version !== 2 || !parsedHandle.projectId) {
|
|
250
|
+
return refusedResult(
|
|
251
|
+
operation,
|
|
252
|
+
toolFailure(
|
|
253
|
+
"invalid",
|
|
254
|
+
"writes require a current project-scoped handle from studio_look",
|
|
255
|
+
"Call studio_look again and pass the returned handle unchanged.",
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (parsedHandle.projectId === projectId) return null;
|
|
260
|
+
return refusedResult(
|
|
261
|
+
operation,
|
|
262
|
+
toolFailure(
|
|
263
|
+
"invalid",
|
|
264
|
+
"the handle belongs to a different project",
|
|
265
|
+
"Call studio_look in the active project.",
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function validateWriteAddress<T extends object>(
|
|
271
|
+
deps: TargetedWriteDeps,
|
|
272
|
+
input: TargetedWriteInput<T>,
|
|
273
|
+
): ValidatedWriteAddress<T> {
|
|
274
|
+
if (input.signal.aborted) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
result: refusedResult(
|
|
278
|
+
input.operation,
|
|
279
|
+
toolFailure("blocked", "the write was cancelled before dispatch"),
|
|
280
|
+
true,
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (typeof input.handle !== "string" || !input.handle) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
result: refusedResult(
|
|
288
|
+
input.operation,
|
|
289
|
+
toolFailure("invalid", "handle must name an element from studio_look"),
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const projectId = deps.getProjectId();
|
|
294
|
+
if (!projectId) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
result: refusedResult(input.operation, toolFailure("blocked", "there is no active project")),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const handleFailure = validateProjectScopedHandle<T>(input.handle, projectId, input.operation);
|
|
301
|
+
if (handleFailure) return { ok: false, result: handleFailure };
|
|
302
|
+
const blocked = deps.getWriteBlockedReason();
|
|
303
|
+
if (blocked) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
result: refusedResult(
|
|
307
|
+
input.operation,
|
|
308
|
+
toolFailure("blocked", blocked, "Resolve it in Studio, then retry."),
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return { ok: true, handle: input.handle, projectId };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type PreparedWriteSelection<T extends object> =
|
|
316
|
+
| { ok: true; selection: DomEditSelection }
|
|
317
|
+
| { ok: false; result: StudioWriteResult<T> };
|
|
318
|
+
|
|
319
|
+
async function prepareWriteSelection<T extends object>(
|
|
320
|
+
deps: TargetedWriteDeps,
|
|
321
|
+
input: TargetedWriteInput<T>,
|
|
322
|
+
handle: string,
|
|
323
|
+
): Promise<PreparedWriteSelection<T>> {
|
|
324
|
+
const resolved = await resolveWriteSelection(deps, handle);
|
|
325
|
+
if (!resolved.ok) {
|
|
326
|
+
return { ok: false, result: refusedResult(input.operation, resolved.failure) };
|
|
327
|
+
}
|
|
328
|
+
const { selection } = resolved;
|
|
329
|
+
if (selection.isInsideLockedComposition) {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
result: refusedResult(
|
|
333
|
+
input.operation,
|
|
334
|
+
toolFailure("blocked", "the target is inside a locked composition"),
|
|
335
|
+
),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const preflight = await input.preflight?.(selection);
|
|
339
|
+
if (preflight) return { ok: false, result: refusedResult(input.operation, preflight) };
|
|
340
|
+
if (input.signal.aborted) {
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
result: refusedResult(
|
|
344
|
+
input.operation,
|
|
345
|
+
toolFailure("blocked", "the write was cancelled before dispatch"),
|
|
346
|
+
true,
|
|
347
|
+
),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return { ok: true, selection };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function runTargetedWrite<T extends object>(
|
|
354
|
+
deps: TargetedWriteDeps,
|
|
355
|
+
input: TargetedWriteInput<T>,
|
|
356
|
+
): Promise<StudioWriteResult<T>> {
|
|
357
|
+
const address = validateWriteAddress(deps, input);
|
|
358
|
+
if (!address.ok) return address.result;
|
|
359
|
+
let prepared: PreparedWriteSelection<T>;
|
|
360
|
+
try {
|
|
361
|
+
prepared = await prepareWriteSelection(deps, input, address.handle);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const { kind, reason } = classifyThrownError(error);
|
|
364
|
+
if (kind === "internal") console.error(`[hf-webmcp] ${input.operation} preflight threw`, error);
|
|
365
|
+
return refusedResult(input.operation, toolFailure(kind, reason));
|
|
366
|
+
}
|
|
367
|
+
if (!prepared.ok) return prepared.result;
|
|
368
|
+
if (deps.getProjectId() !== address.projectId) {
|
|
369
|
+
return refusedResult(
|
|
370
|
+
input.operation,
|
|
371
|
+
toolFailure(
|
|
372
|
+
"blocked",
|
|
373
|
+
"the active project changed while the target was resolving",
|
|
374
|
+
"Call studio_look in the active project and retry.",
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const { selection } = prepared;
|
|
379
|
+
const target = { handle: address.handle, sourceFile: selection.sourceFile };
|
|
380
|
+
deps.applySelection(selection);
|
|
381
|
+
const callId = studioEditLifecycle.begin(address.projectId, target, input.operation);
|
|
382
|
+
let adapter: StudioWriteAdapterResult<T>;
|
|
383
|
+
try {
|
|
384
|
+
adapter = await input.write(selection);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const { kind, reason } = classifyThrownError(error);
|
|
387
|
+
if (kind === "internal") console.error(`[hf-webmcp] ${input.operation} threw`, error);
|
|
388
|
+
const result: StudioWriteResult<T> = {
|
|
389
|
+
ok: false,
|
|
390
|
+
kind,
|
|
391
|
+
reason,
|
|
392
|
+
stage: "failed",
|
|
393
|
+
target,
|
|
394
|
+
operation: input.operation,
|
|
395
|
+
...(input.signal.aborted ? { cancelRequested: true as const } : {}),
|
|
396
|
+
};
|
|
397
|
+
studioEditLifecycle.finish(callId, result);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
const result = completedWrite(adapter, target, input.operation, input.signal.aborted);
|
|
401
|
+
studioEditLifecycle.finish(callId, result);
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function asFailed(failure: ToolFailure): ToolFailure {
|
|
406
|
+
const kind: ToolFailureKind =
|
|
407
|
+
failure.kind === "invalid" || failure.kind === "blocked" ? "failed" : failure.kind;
|
|
408
|
+
return { ...failure, kind };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function dispatched<T extends object>(
|
|
412
|
+
value: T,
|
|
413
|
+
changed: boolean,
|
|
414
|
+
): StudioWriteAdapterSuccess<T> & {
|
|
415
|
+
stage: "dispatched";
|
|
416
|
+
evidence: { kind: "dispatch"; followUp: "studio_inspect" };
|
|
417
|
+
} {
|
|
418
|
+
return {
|
|
419
|
+
ok: true,
|
|
420
|
+
...value,
|
|
421
|
+
stage: "dispatched",
|
|
422
|
+
changed,
|
|
423
|
+
evidence: { kind: "dispatch", followUp: "studio_inspect" },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function saved<T extends object>(
|
|
428
|
+
value: T,
|
|
429
|
+
persistence: { sourceFile: string; version: string; changed: boolean },
|
|
430
|
+
): StudioWriteAdapterSuccess<T> & {
|
|
431
|
+
stage: "saved";
|
|
432
|
+
evidence: { kind: "content-version"; sourceFile: string; version: string };
|
|
433
|
+
} {
|
|
434
|
+
return {
|
|
435
|
+
ok: true,
|
|
436
|
+
...value,
|
|
437
|
+
stage: "saved",
|
|
438
|
+
changed: persistence.changed,
|
|
439
|
+
evidence: {
|
|
440
|
+
kind: "content-version",
|
|
441
|
+
sourceFile: persistence.sourceFile,
|
|
442
|
+
version: persistence.version,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function verified<T extends object>(
|
|
448
|
+
value: T,
|
|
449
|
+
persistence: { sourceFile: string; version: string; changed: boolean },
|
|
450
|
+
readback: { before: unknown; after: unknown },
|
|
451
|
+
): StudioWriteAdapterSuccess<T> & {
|
|
452
|
+
stage: "verified";
|
|
453
|
+
evidence: {
|
|
454
|
+
kind: "readback";
|
|
455
|
+
sourceFile: string;
|
|
456
|
+
version: string;
|
|
457
|
+
before: unknown;
|
|
458
|
+
after: unknown;
|
|
459
|
+
};
|
|
460
|
+
} {
|
|
461
|
+
return {
|
|
462
|
+
ok: true,
|
|
463
|
+
...value,
|
|
464
|
+
stage: "verified",
|
|
465
|
+
changed: persistence.changed,
|
|
466
|
+
evidence: {
|
|
467
|
+
kind: "readback",
|
|
468
|
+
sourceFile: persistence.sourceFile,
|
|
469
|
+
version: persistence.version,
|
|
470
|
+
...readback,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|