cozyclay 2.0.2 → 2.1.1

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.
Files changed (262) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +23 -8
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-C9pMpW7s.js +1 -0
  26. package/dist/assets/app-B_2XB9rw.css +1 -0
  27. package/dist/assets/app-DAuvKPP8.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
  34. package/dist/assets/workflow-jJRBR94Z.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/llms.txt +14 -0
  38. package/dist/sitemap.xml +1 -1
  39. package/dist/workflow/index.html +4 -4
  40. package/mcp/LIVE-PROTOCOL.md +23 -2
  41. package/mcp/README.md +37 -2
  42. package/mcp/ardy-prompts.mjs +1 -1
  43. package/mcp/image-file.mjs +132 -0
  44. package/mcp/live-hub.mjs +2 -2
  45. package/mcp/package.json +1 -0
  46. package/mcp/runtime/package-lock.json +7 -0
  47. package/mcp/runtime/package.json +1 -0
  48. package/mcp/server.mjs +0 -6
  49. package/mcp/tool-handlers.mjs +289 -87
  50. package/package.json +11 -2
  51. package/src/App.jsx +813 -278
  52. package/src/agent-image-client.js +94 -0
  53. package/src/analytics.js +47 -3
  54. package/src/app-stage.jsx +129 -2745
  55. package/src/ardy/auto-physics.js +6 -16
  56. package/src/ardy/collision-blockers.js +3 -7
  57. package/src/ardy/fix-collisions.js +5 -35
  58. package/src/ardy/ik-drag.js +86 -5
  59. package/src/ardy/ik-key-json.js +3 -1
  60. package/src/ardy/ik.js +15 -13
  61. package/src/ardy/motion-calibration.js +1 -1
  62. package/src/ardy/npz.js +0 -8
  63. package/src/ardy/physics-panel.jsx +3 -2
  64. package/src/ardy/physics-review.js +3 -2
  65. package/src/ardy/physics-support.js +1 -1
  66. package/src/ardy/physics-surface.js +1 -1
  67. package/src/ardy/platform-fit-panel.jsx +2 -1
  68. package/src/ardy/playback.js +1 -5
  69. package/src/ardy/range-pin.js +2 -2
  70. package/src/ardy/root-drop.js +0 -1
  71. package/src/ardy/timeline.css +184 -6
  72. package/src/ardy/timeline.jsx +121 -36
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +0 -3
  75. package/src/ardy-bridge-contract.js +281 -0
  76. package/src/asset-pane.jsx +19 -9
  77. package/src/auto-color.js +1 -1
  78. package/src/burn-in.js +1 -1
  79. package/src/camera-block.js +1 -1
  80. package/src/camera-follow.js +75 -32
  81. package/src/camera-move.js +0 -6
  82. package/src/camera-tutorial.jsx +6 -7
  83. package/src/character-kind.js +79 -0
  84. package/src/command-bus.js +76 -17
  85. package/src/commands/cast.js +31 -7
  86. package/src/commands/elements/object.js +1 -1
  87. package/src/commands/elements/shot.js +1 -1
  88. package/src/commands/elements.js +1 -1
  89. package/src/commands/export.js +3 -3
  90. package/src/commands/motion.js +2 -2
  91. package/src/commands/objects.js +105 -10
  92. package/src/commands/project.js +15 -2
  93. package/src/commands/sculpt-images.js +53 -0
  94. package/src/commands/shot.js +88 -10
  95. package/src/commands/view.js +10 -4
  96. package/src/coplanar-depth.js +216 -0
  97. package/src/cuts.js +7 -6
  98. package/src/domains/cast.js +60 -43
  99. package/src/domains/motion/motion-intent-codec.js +50 -0
  100. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  101. package/src/domains/motion/motion-video-capture.js +664 -0
  102. package/src/domains/motion.js +253 -742
  103. package/src/domains/objects.js +106 -5
  104. package/src/domains/scenes.js +79 -28
  105. package/src/domains/shots.js +33 -7
  106. package/src/error-boundary.jsx +1 -1
  107. package/src/execution-telemetry.js +5 -5
  108. package/src/facing-marks.js +14 -1
  109. package/src/first-success-guide.jsx +35 -4
  110. package/src/gizmo-claim.js +3 -1
  111. package/src/grid-floor.jsx +1 -4
  112. package/src/hierarchy-model.js +48 -35
  113. package/src/hierarchy-panel.css +1 -0
  114. package/src/hierarchy-panel.jsx +54 -102
  115. package/src/history.js +0 -2
  116. package/src/i2v-motion-client.js +1 -4
  117. package/src/ime.js +6 -0
  118. package/src/keyframe-pack.js +15 -6
  119. package/src/line-edit.js +4 -4
  120. package/src/live-control.js +6 -6
  121. package/src/matte-editor.js +2 -8
  122. package/src/matte.js +0 -19
  123. package/src/mesh-graph-clone.js +16 -2
  124. package/src/mesh-sniff.js +0 -1
  125. package/src/monotone-cubic.js +40 -0
  126. package/src/motion-store.js +4 -4
  127. package/src/motion-trail.js +5 -11
  128. package/src/multimodel-ingest.js +1 -3
  129. package/src/object-catalog.jsx +7 -5
  130. package/src/object-gizmo.jsx +45 -31
  131. package/src/object-path.js +310 -67
  132. package/src/object-travel.js +224 -0
  133. package/src/otio.js +8 -3
  134. package/src/panels/CharacterTransformPanel.jsx +24 -6
  135. package/src/panels/ObjectTransformPanel.jsx +4 -3
  136. package/src/panels/PoseImagePanel.jsx +205 -0
  137. package/src/panels/PromptBlocksPanel.jsx +3 -1
  138. package/src/panels/ProxyFigurePanel.jsx +56 -0
  139. package/src/panels/ReferenceImageField.jsx +1 -1
  140. package/src/panels/RigControlPanel.jsx +24 -3
  141. package/src/panels/RigPanel.jsx +2 -1
  142. package/src/panels/SculptPanel.jsx +405 -0
  143. package/src/panels/SubjectBox.jsx +1 -1
  144. package/src/panels/SubjectsPanel.jsx +0 -2
  145. package/src/panels/VideoCapturePanel.jsx +2 -1
  146. package/src/panels/pose-image.css +42 -0
  147. package/src/panels/pose.css +3 -0
  148. package/src/panels/sculpt.css +162 -0
  149. package/src/plan-diagram.js +184 -0
  150. package/src/planview.jsx +127 -80
  151. package/src/playground.js +3 -5
  152. package/src/pose-fade.js +77 -0
  153. package/src/pose-gizmo.css +23 -0
  154. package/src/pose-gizmo.js +96 -0
  155. package/src/posestudio.jsx +134 -83
  156. package/src/project-browser.css +53 -0
  157. package/src/project-browser.jsx +49 -4
  158. package/src/project.js +55 -24
  159. package/src/props.jsx +177 -36
  160. package/src/proxy-figure.jsx +69 -0
  161. package/src/range-pin-panel.jsx +0 -1
  162. package/src/receipt-findings.js +20 -0
  163. package/src/render-passes.js +0 -1
  164. package/src/resource-status.jsx +1 -1
  165. package/src/root-path.js +48 -0
  166. package/src/route-owner.js +52 -0
  167. package/src/scene-asset-cache.js +1 -2
  168. package/src/scene-assets.js +12 -4
  169. package/src/scene-fbx.js +1 -1
  170. package/src/scene-mesh-cache.js +0 -1
  171. package/src/scene-objects.js +319 -19
  172. package/src/scenes.js +58 -9
  173. package/src/sculpt-blueprint.js +65 -0
  174. package/src/sculpt-compare.js +384 -0
  175. package/src/sculpt-geometry.js +141 -0
  176. package/src/sculpt-mode.js +168 -0
  177. package/src/sculpt-recipe.js +385 -0
  178. package/src/sculpt-run.js +351 -0
  179. package/src/sculpt-stage.jsx +221 -0
  180. package/src/settings-menu.jsx +1 -1
  181. package/src/shell/BoardSlot.jsx +341 -0
  182. package/src/shell/BottomDock.jsx +60 -16
  183. package/src/shell/CompositionGuideButton.jsx +20 -0
  184. package/src/shell/DetailsSlot.jsx +26 -6
  185. package/src/shell/MenuBar.jsx +78 -6
  186. package/src/shell/OutlinerSlot.jsx +3 -1
  187. package/src/shell/ShotPreviewHeader.jsx +17 -0
  188. package/src/shell/TopBar.jsx +18 -3
  189. package/src/shell/ViewportToolbar.jsx +36 -31
  190. package/src/shell/dock.css +239 -0
  191. package/src/shell/glass-regions.css +3 -2
  192. package/src/shell/log-store.js +4 -4
  193. package/src/shell/mode.css +20 -0
  194. package/src/shell/studio-shell-context.js +4 -1
  195. package/src/shell/viewport.css +12 -0
  196. package/src/shot-authoring.js +69 -6
  197. package/src/stable-items.js +1 -1
  198. package/src/stage-attach-frames.js +169 -0
  199. package/src/stage-canvas-rigs.jsx +402 -0
  200. package/src/stage-capture.jsx +216 -0
  201. package/src/stage-characters.jsx +302 -0
  202. package/src/stage-editor-gizmos.jsx +1229 -0
  203. package/src/stage-labels-ko.js +204 -0
  204. package/src/stage-startup-defaults.js +182 -0
  205. package/src/starter-scenes.js +1 -1
  206. package/src/storyboard.js +24 -5
  207. package/src/studio-actions.js +42 -9
  208. package/src/studio-agent-commands.js +213 -23
  209. package/src/studio-agent-context.js +11 -9
  210. package/src/studio-agent-motion.js +2 -1
  211. package/src/studio-agent-protocol.js +60 -24
  212. package/src/studio-app-binding.js +56 -23
  213. package/src/studio-contact-sheet.js +3 -3
  214. package/src/studio-elements.js +11 -2
  215. package/src/studio-geometry-facts.js +114 -0
  216. package/src/styles.css +92 -9
  217. package/src/ui-scrub.js +4 -4
  218. package/src/ui.jsx +6 -4
  219. package/src/use-asset-shelf.js +60 -0
  220. package/src/use-camera-tutorial.js +50 -0
  221. package/src/use-embed-bridge.js +12 -0
  222. package/src/use-first-success-prompt.js +7 -0
  223. package/src/use-i2v-capture-modal.js +15 -0
  224. package/src/use-sculpt-mode.js +95 -0
  225. package/src/workflow/AgentPanel.jsx +106 -18
  226. package/src/workflow/WorkflowBuilder.jsx +1 -1
  227. package/src/workflow/agent-client.js +113 -45
  228. package/src/workflow/agent-panel.css +35 -0
  229. package/src/workflow/attachment-image.js +1 -1
  230. package/src/workflow/canvas-commands.js +3 -3
  231. package/src/workflow/cozy-scene-node.js +3 -3
  232. package/src/workflow/local-workflow.js +2 -2
  233. package/src/workflow/motion-input.js +3 -3
  234. package/src/workflow/node-schema.js +1 -1
  235. package/src/workflow/scene-asset-sync.js +2 -9
  236. package/src/workflow/shot-prompt-node.js +1 -1
  237. package/src/workflow/video-contract.js +1 -1
  238. package/src/workflow/workflow-resources.js +0 -8
  239. package/src/workflow-send.js +87 -0
  240. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  241. package/tools/check-eslint.mjs +57 -0
  242. package/tools/check-knip.mjs +60 -0
  243. package/tools/check-loc.mjs +54 -0
  244. package/tools/demo-worker/api-client.mjs +7 -13
  245. package/tools/demo-worker/generate.mjs +2 -4
  246. package/tools/eslint-baseline.json +23 -0
  247. package/tools/gt-render/browser.mjs +23 -0
  248. package/tools/headless-studio.mjs +85 -0
  249. package/tools/kimodo/effector-constraints.mjs +4 -4
  250. package/tools/kimodo/generate.mjs +1 -1
  251. package/tools/knip-baseline.json +331 -0
  252. package/tools/loc-baseline.json +126 -0
  253. package/tools/process-supervisor.mjs +1 -1
  254. package/tools/projflow/replay.mjs +4 -4
  255. package/tools/projflow/runner.mjs +3 -3
  256. package/tools/qa/studio-control-count.mjs +2 -0
  257. package/tools/run-tests.mjs +95 -7
  258. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  259. package/dist/assets/app-BxBLC6GT.js +0 -4876
  260. package/dist/assets/app-CJdxvnrl.css +0 -1
  261. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  262. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -13,7 +13,7 @@
13
13
  // POST /oauth/logout -> { ok }
14
14
  // POST /agent/turn -> SSE, lines of `data: {json}`
15
15
  // POST /agent/stop -> { ok }
16
- // GET /agent/models -> { providers: [{ id, label, signedIn, models }], models: [flat] }
16
+ // GET /agent/models -> { providers: [{ id, label, signedIn, models }], models: [flat], roles: { byMain } }
17
17
  // POST /agent/turn/<turnId>/steer -> { ok, queued } while that turn streams
18
18
  //
19
19
  // Studio hosts add the frozen task-1 contracts on the same routes:
@@ -21,14 +21,16 @@
21
21
  // GET /agent/turn/<turnId>/events?after=N -> replay after a dropped stream
22
22
  // POST /agent/stop -> { surface, sessionId, turnId, jobId? }
23
23
  // POST /agent/jobs/<jobId>/accept -> explicit "Apply with warnings"
24
+ // GET /agent/turn/<turnId>/supervisor -> 200 supervisor note | 204 none (#728)
24
25
 
25
26
  import { bucketMs, track } from "../analytics.js";
26
27
  import { appendAttachments, ATTACHMENT_MAX_COUNT } from "./attachment-image.js";
27
28
  import { ko } from "../locale.js";
28
29
  import { AGENT_TOOL_CATEGORIES, EXECUTION_TELEMETRY_VALUES } from "../execution-telemetry.js";
29
30
  import { STUDIO_VARIANTS, validateReceipt } from "../studio-agent-protocol.js";
31
+ import { requestAgentImage, sidecarRequest, sidecarUrl } from "../agent-image-client.js";
30
32
 
31
- export const AGENT_PANEL_WIDTH_KEY = "cozyclay.workflow.agentPanel.width";
33
+ const AGENT_PANEL_WIDTH_KEY = "cozyclay.workflow.agentPanel.width";
32
34
  export const AGENT_PANEL_WIDTH_DEFAULT = 360;
33
35
  export const AGENT_PANEL_WIDTH_MIN = 300;
34
36
  export const AGENT_PANEL_WIDTH_MAX = 560;
@@ -125,7 +127,7 @@ export const SUGGESTION_CHIPS = [
125
127
  /** Previs asks, in the Studio's own words: short enough to read at a glance in
126
128
  * the Inspector column, and every one of them is something the Studio families
127
129
  * can actually do. */
128
- export const STUDIO_SUGGESTION_CHIPS = [
130
+ const STUDIO_SUGGESTION_CHIPS = [
129
131
  ko("Block a two-shot", "\uD22C\uC0F7 \uBE14\uB85C\uD0B9"),
130
132
  ko("Frame the selected character", "\uC120\uD0DD\uD55C \uCE90\uB9AD\uD130 \uAD6C\uB3C4 \uC7A1\uAE30"),
131
133
  ko("Light the set warmer", "\uC138\uD2B8 \uC870\uBA85 \uB354 \uB530\uB73B\uD558\uAC8C"),
@@ -263,6 +265,27 @@ export function storeModel(key, storage = globalThis.localStorage) {
263
265
  }
264
266
  }
265
267
 
268
+ /** The supervisor switch (#728): on unless the author turned it off. It rides
269
+ * on every Studio turn as `supervisor: false` so the sidecar skips the review
270
+ * for that turn; the sidecar's own COZYCLAY_SUPERVISOR=off still wins. */
271
+ const AGENT_SUPERVISOR_KEY = "cozyclay.agent.supervisor";
272
+
273
+ export function readStoredSupervisor(storage = globalThis.localStorage) {
274
+ try {
275
+ return storage?.getItem(AGENT_SUPERVISOR_KEY) !== "off";
276
+ } catch {
277
+ return true;
278
+ }
279
+ }
280
+
281
+ export function storeSupervisor(enabled, storage = globalThis.localStorage) {
282
+ try {
283
+ storage?.setItem(AGENT_SUPERVISOR_KEY, enabled ? "on" : "off");
284
+ } catch {
285
+ // Private-mode storage denial must never break the toggle.
286
+ }
287
+ }
288
+
266
289
  /** The model to open with: the remembered key when it is still advertised,
267
290
  * otherwise the first model the sidecar listed. */
268
291
  export function preferredModel(models, stored = readStoredModel()) {
@@ -293,6 +316,20 @@ export function nextSelectedModel(providers, models, current) {
293
316
  return preferredModel(models.filter((entry) => modelIsSelectable(providers, entry.id)));
294
317
  }
295
318
 
319
+ /** The roles the sidecar derived for one main model (#717): `{ helper, vision,
320
+ * advisor, fallback }`, each a `provider/id` key. Read-only here — nothing the
321
+ * panel sends depends on it — so a malformed entry is simply not shown. */
322
+ export function selectedModelRoles(byMain, key) {
323
+ const entry = byMain && typeof byMain === "object" && typeof key === "string" ? byMain[key] : null;
324
+ return entry && ["helper", "vision", "advisor"].every((role) => typeof entry[role] === "string" && entry[role]) ? entry : null;
325
+ }
326
+
327
+ /** A `provider/id` key without its provider, for a line that has no room for it. */
328
+ export function shortModelId(key) {
329
+ const value = String(key ?? "");
330
+ return value.slice(value.indexOf("/") + 1);
331
+ }
332
+
296
333
  /** "resets in 42m" / "resets in 1h 05m" for the account strip and the paused
297
334
  * card countdown. Returns null when there is nothing to count down to. */
298
335
  export function formatResetIn(resetAt, now = Date.now()) {
@@ -340,6 +377,28 @@ export const STEER_ERROR_COPY = {
340
377
  NO_ACTIVE_TURN: "That turn already ended — send it as a new message.",
341
378
  };
342
379
 
380
+ /** Codes a Studio refusal carries: a turn that met one is reviewed by the
381
+ * supervisor even when it authored nothing (#728). */
382
+ const SUPERVISOR_REFUSAL = /^(TARGET_NOT_READY|CAPABILITY_MISSING|CONFIRMATION_REQUIRED):/;
383
+
384
+ /** The next prompt a supervisor note suggests: its blocker and concern issues,
385
+ * or null when it raised none. A note with validated fixes (#743) asks for
386
+ * exactly those run_action calls, in order, in one script; the blockers and
387
+ * concerns without a fix follow under their own header. */
388
+ export function supervisorFollowUp(note) {
389
+ const all = Array.isArray(note?.issues) ? note.issues : [];
390
+ const fixes = (Array.isArray(note?.fixes) ? note.fixes : []).filter((fix) => typeof fix?.action === "string" && fix.args && typeof fix.args === "object");
391
+ const fixed = new Set(fixes.map((fix) => fix.issueIndex));
392
+ const issues = all.filter((issue, index) => (issue?.severity === "blocker" || issue?.severity === "concern") && !fixed.has(index));
393
+ const line = (issue) => `- [${issue.severity}] ${issue.text}${issue.evidence ? ` (${issue.evidence})` : ""}`;
394
+ if (!fixes.length) return issues.length ? [ko("Supervisor review — please address:", "감독 검토 — 다음을 고쳐 주세요:"), ...issues.map(line)].join("\n") : null;
395
+ return [
396
+ ko("Supervisor review — apply these fixes exactly, in order, in ONE script call, then verify_result; do not re-derive them:", "감독 검토 — 아래 수정을 유추 없이 순서대로 script 1회로 실행하고 verify_result로 확인해 주세요:"),
397
+ ...fixes.map((fix) => `- [${fix.severity}] ${all[fix.issueIndex]?.text ?? ""} -> run_action ${JSON.stringify({ action: fix.action, args: fix.args })}`),
398
+ ...(issues.length ? [ko("Also check:", "추가 확인:"), ...issues.map(line)] : []),
399
+ ].join("\n");
400
+ }
401
+
343
402
  export const ERROR_COPY = {
344
403
  auth: "Your session expired. Sign in again to continue.",
345
404
  entitlement: "This account cannot generate images.",
@@ -348,8 +407,6 @@ export const ERROR_COPY = {
348
407
  overloaded: "The model service is overloaded right now. Try again in a moment.",
349
408
  };
350
409
 
351
- // --- activity line ---------------------------------------------------------
352
- //
353
410
  // Silence is the failure mode this replaces: a live turn says what it is doing
354
411
  // and a finished turn says how it ended, in the same line, so "nothing on
355
412
  // screen" can never mean "nobody knows".
@@ -445,14 +502,6 @@ export function parseSseChunk(buffer) {
445
502
  return { events, tail };
446
503
  }
447
504
 
448
- // --- real transport (loopback sidecar) ------------------------------------
449
-
450
- const SIDECAR_ORIGIN = "";
451
-
452
- function sidecarUrl(path) {
453
- return `${SIDECAR_ORIGIN}${path}`;
454
- }
455
-
456
505
  const validTelemetryId = (value) => typeof value === "string" && /^[a-f0-9]{32}$/.test(value);
457
506
  const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]);
458
507
  const exactKeys = (value, keys) => value && typeof value === "object" && !Array.isArray(value)
@@ -574,7 +623,7 @@ const RESUME_ATTEMPTS = 3;
574
623
  * — it only needs the variable's NAME, so an env-backed provider can say which
575
624
  * variable is already holding one instead of offering an input that would be
576
625
  * ignored. */
577
- export const PROVIDER_ENV_VARS = {
626
+ const PROVIDER_ENV_VARS = {
578
627
  anthropic: ["ANTHROPIC_API_KEY"],
579
628
  openai: ["OPENAI_API_KEY"],
580
629
  google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
@@ -590,26 +639,7 @@ export function providerEnvLabel(id) {
590
639
 
591
640
  export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalThis), surface, capture = track, now = () => performance.now() } = {}) {
592
641
  const activeTurns = new Map();
593
- const request = async (path, init) => {
594
- const response = await fetchImpl(sidecarUrl(path), {
595
- headers: { "content-type": "application/json" },
596
- ...init,
597
- });
598
- if (!response.ok) {
599
- let detail = null;
600
- try { detail = await response.clone().json(); } catch { /* preserve the status when the server did not send JSON */ }
601
- const message = typeof detail?.error === "string" ? detail.error : detail?.error?.message;
602
- const error = new Error(message || `${path} responded ${response.status}`);
603
- // Keep machine-readable verification evidence alongside the human message.
604
- // The Workflow node can show why an H3 take was rejected without exposing
605
- // or retaining the rejected video itself.
606
- error.status = response.status;
607
- if (detail?.error && typeof detail.error === "object") Object.assign(error, detail.error);
608
- if (detail?.preservation && typeof detail.preservation === "object") error.preservation = detail.preservation;
609
- throw error;
610
- }
611
- return response.json();
612
- };
642
+ const request = (path, init) => sidecarRequest(fetchImpl, path, init);
613
643
  return {
614
644
  mock: false,
615
645
  async status() {
@@ -635,6 +665,10 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
635
665
  const providers = Array.isArray(result?.providers)
636
666
  ? result.providers.filter((provider) => provider?.id && Array.isArray(provider.models))
637
667
  : [];
668
+ // The derived model roles (#717) ride along untouched; a sidecar without
669
+ // them simply leaves the roles line hidden.
670
+ const byMain = result?.roles?.byMain;
671
+ if (byMain && typeof byMain === "object" && !Array.isArray(byMain)) return { providers, models, roles: { byMain } };
638
672
  return { providers, models };
639
673
  },
640
674
  /** Steering a turn that is STILL streaming. The id is the one this browser
@@ -682,8 +716,8 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
682
716
  // `references` are the scene's identity / environment slots (#167): extra
683
717
  // attached pictures with a role, passed through untouched so the sidecar
684
718
  // decides how they are described to the model.
685
- async image({ prompt, imageDataUrl, referenceDataUrl, references, quality = "auto" }, signal) {
686
- return request("/agent/image", { method: "POST", body: JSON.stringify({ prompt, imageDataUrl, ...(referenceDataUrl ? { referenceDataUrl } : {}), ...(Array.isArray(references) && references.length ? { references } : {}), quality }), signal });
719
+ async image(payload, signal) {
720
+ return requestAgentImage(payload, { fetchImpl, signal });
687
721
  },
688
722
  async video(payload, signal) {
689
723
  return request("/agent/video", { method: "POST", body: JSON.stringify(payload), signal });
@@ -691,9 +725,18 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
691
725
  async videoProviders() {
692
726
  return request("/agent/video/providers");
693
727
  },
728
+ /** The supervisor note for a finished Studio turn (#728): one long-poll
729
+ * the sidecar holds while the review runs. A 200 is the note; a 204, a
730
+ * failure or anything unreadable is no note. */
731
+ async supervisor(turnId, signal) {
732
+ const response = await fetchImpl(sidecarUrl(`/agent/turn/${encodeURIComponent(turnId)}/supervisor`), { headers: { accept: "application/json" }, credentials: "same-origin", signal });
733
+ if (response?.status !== 200) return null;
734
+ const note = await response.json();
735
+ return note?.type === "supervisor" && ["reviewed", "unavailable"].includes(note.verdict) ? note : null;
736
+ },
694
737
  /** Streams sidecar events to `onEvent`. Resolves when the turn ends. */
695
738
  async turn(turnRequest, onEvent, signal) {
696
- const { sessionId, text, attachFrame, model, effort, attachments } = turnRequest;
739
+ const { sessionId, text, attachFrame, model, effort, attachments, supervisor } = turnRequest;
697
740
  const attached = Array.isArray(attachments) && attachments.length ? { attachments } : {};
698
741
  const studio = turnRequest.surface === "studio";
699
742
  const turnId = studio ? turnRequest.turnId : null;
@@ -748,7 +791,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
748
791
  const body = studio
749
792
  ? JSON.stringify({
750
793
  surface: "studio", sessionId, turnId, text, context: turnRequest.context,
751
- ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(attachFrame === undefined ? {} : { attachFrame: Boolean(attachFrame) }), ...attached,
794
+ ...(model ? { model } : {}), ...(effort ? { effort } : {}), ...(attachFrame === undefined ? {} : { attachFrame: Boolean(attachFrame) }), ...(supervisor === false ? { supervisor: false } : {}), ...attached,
752
795
  })
753
796
  : JSON.stringify({ sessionId, text, attachFrame, model, ...(effort ? { effort } : {}), ...attached, ...(telemetry.turnId ? { turn_id: telemetry.turnId } : {}) });
754
797
  try {
@@ -798,8 +841,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
798
841
  };
799
842
  }
800
843
 
801
- // --- mock transport (QA) ---------------------------------------------------
802
-
844
+ // --- mock transport (QA) ---
803
845
  /** ?agent=mock turns the panel onto a scripted transport so every state in the
804
846
  * issue can be reached and screenshotted before the sidecar exists. */
805
847
  export function mockConfigFromSearch(search = globalThis.location?.search || "") {
@@ -902,7 +944,7 @@ const MOCK_PROVIDER_MODELS = {
902
944
  const MOCK_LIVE_ONLY_CODEX_MODEL = { id: "gpt-6-live-preview", label: "gpt-6 live preview", efforts: ["none", "low", "medium", "high"], defaultEffort: "high" };
903
945
  /** The shortest key the scripted sidecar will store, so QA can drive the
904
946
  * refusal path (PUT → 400) without a real provider. */
905
- export const MOCK_PROVIDER_KEY_MIN = 8;
947
+ const MOCK_PROVIDER_KEY_MIN = 8;
906
948
  /** How many times the scripted sidecar answered /oauth/status. The panel reads
907
949
  * the session once per credential write and never on a timer (#379), and this
908
950
  * is how browser QA proves it without reaching inside the component. */
@@ -1107,8 +1149,6 @@ export function createAgentTransport(options = {}) {
1107
1149
  return config ? createMockTransport({ surface: options.surface, ...config }) : createHttpTransport(options);
1108
1150
  }
1109
1151
 
1110
- // --- chat store (framework-free) -------------------------------------------
1111
- //
1112
1152
  // One conversation: session identity, transcript, motion jobs, receipts and
1113
1153
  // acknowledged image actions. It lives here rather than inside the component so
1114
1154
  // the dock, an embedded Studio host and the test runner drive the SAME state
@@ -1119,7 +1159,7 @@ export function createAgentTransport(options = {}) {
1119
1159
  * already committed belongs to its receipt, not here; anything short of an
1120
1160
  * explicit not-applied answer stays unknown, because the alternative is telling
1121
1161
  * an author the scene is clean when nobody checked. */
1122
- export function stopOutcome(result) {
1162
+ function stopOutcome(result) {
1123
1163
  const outcome = result?.outcome;
1124
1164
  if (outcome?.status === "already_applied") return null;
1125
1165
  if (outcome && outcome.mutated === false) return { status: "not_applied", code: outcome.code ?? "CANCELLED", mutated: false };
@@ -1171,6 +1211,9 @@ export function createAgentChatStore({
1171
1211
  // turnId, or the hex id the dock's transport mints. It is what the steer
1172
1212
  // route is keyed by, and only the transport can tell us which it is.
1173
1213
  let wireTurnId = null;
1214
+ // Supervisor note requests still waiting on the sidecar; a cleared or
1215
+ // replaced conversation abandons them.
1216
+ const supervisorRequests = new Set();
1174
1217
  let state = {
1175
1218
  sessionId: newId(),
1176
1219
  turnId: null,
@@ -1262,8 +1305,29 @@ export function createAgentChatStore({
1262
1305
  try { onReceipt?.(receipt); } catch { /* host presentation is advisory */ }
1263
1306
  }
1264
1307
 
1308
+ // One request per reviewable Studio turn, after its stream ended: the note
1309
+ // joins the transcript whenever it arrives, and nothing waits for it.
1310
+ function requestSupervisorNote(turnId) {
1311
+ if (typeof transport.supervisor !== "function") return;
1312
+ const request = new AbortController();
1313
+ supervisorRequests.add(request);
1314
+ Promise.resolve()
1315
+ .then(() => transport.supervisor(turnId, request.signal))
1316
+ .then((note) => {
1317
+ if (!note || request.signal.aborted) return;
1318
+ setItems((items) => [...items, { ...note, kind: "supervisor", id: `supervisor:${turnId}` }]);
1319
+ })
1320
+ .catch(() => { /* a missing note is no note */ })
1321
+ .finally(() => supervisorRequests.delete(request));
1322
+ }
1323
+ const abandonSupervisorNotes = () => { for (const request of supervisorRequests) request.abort(); supervisorRequests.clear(); };
1324
+
1265
1325
  function applyEvent(event) {
1266
1326
  if (activeTurn && ["text.delta", "tool.start", "tool.done", "image", "job.state", "job.progress", "receipt", "error"].includes(event?.type)) activeTurn.produced = true;
1327
+ // The supervisor's own trigger, read from the same frames: an authored
1328
+ // result or a refusal makes the turn reviewable.
1329
+ if (activeTurn && ((event?.type === "tool.done" && (event.ok ? event.result?.authored === true : SUPERVISOR_REFUSAL.test(String(event.error ?? ""))))
1330
+ || (event?.type === "receipt" && event.receipt?.authored === true))) activeTurn.reviewable = true;
1267
1331
  if (event?.type === "text.delta") {
1268
1332
  setItems((items) => {
1269
1333
  const last = items[items.length - 1];
@@ -1341,6 +1405,7 @@ export function createAgentChatStore({
1341
1405
  }
1342
1406
 
1343
1407
  const resetSession = () => {
1408
+ abandonSupervisorNotes();
1344
1409
  seenReceipts.clear();
1345
1410
  settledActions.clear();
1346
1411
  // Clearing the transcript also retires the server session: a cleared chat
@@ -1348,6 +1413,7 @@ export function createAgentChatStore({
1348
1413
  set({ sessionId: newId(), turnId: null, items: [], pendingAttachments: [], rateLimit: null, lastPrompt: "", turnStartedAt: null, lastTurn: null });
1349
1414
  };
1350
1415
  const restore = (transcript, sessionId = state.sessionId) => {
1416
+ abandonSupervisorNotes();
1351
1417
  seenReceipts.clear();
1352
1418
  settledActions.clear();
1353
1419
  const restoredReceiptIds = new Set();
@@ -1404,7 +1470,7 @@ export function createAgentChatStore({
1404
1470
  return;
1405
1471
  }
1406
1472
  const startedAt = clock();
1407
- const turn = { produced: false, failure: null };
1473
+ const turn = { produced: false, failure: null, reviewable: false };
1408
1474
  activeTurn = turn;
1409
1475
  // What the author attached to THIS message: an explicit list wins, the
1410
1476
  // composer's pending pictures otherwise. Either way the composer is
@@ -1435,6 +1501,7 @@ export function createAgentChatStore({
1435
1501
  attachFrame: Boolean(options.attachFrame),
1436
1502
  model: options.model,
1437
1503
  effort: options.effort ?? undefined,
1504
+ ...(options.supervisor === false ? { supervisor: false } : {}),
1438
1505
  }, applyEvent, signal);
1439
1506
  } catch (error) {
1440
1507
  if (!signal.aborted) applyEvent({ type: "error", code: "upstream", message: String(error?.message || error) });
@@ -1460,6 +1527,7 @@ export function createAgentChatStore({
1460
1527
  failure: turn.failure,
1461
1528
  },
1462
1529
  });
1530
+ if (studio && turn.reviewable && options.supervisor !== false && !signal.aborted) requestSupervisorNote(turnId);
1463
1531
  }
1464
1532
  }
1465
1533
  },
@@ -744,6 +744,16 @@
744
744
 
745
745
  .agent-receipt-summary { line-height: 1.5; word-break: break-word; }
746
746
 
747
+ /* The supervisor note (#728) reads quieter than the turn it reviews. */
748
+ .agent-supervisor-card { display: flex; flex-direction: column; gap: var(--agent-space-2); padding: var(--agent-space-3) var(--agent-space-4); border-style: dashed; background: var(--agent-bg-sunken); color: var(--agent-text-muted); font-size: var(--agent-font-md); }
749
+ .agent-supervisor-head { display: flex; align-items: center; gap: var(--agent-space-2); }
750
+ .agent-supervisor-toggle { margin-left: auto; }
751
+ .agent-supervisor-summary { margin: 0; line-height: 1.5; word-break: break-word; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
752
+ .agent-supervisor-card[data-open="true"] .agent-supervisor-summary { display: block; }
753
+ .agent-supervisor-badge.blocker { border-color: var(--agent-alert-edge); background: var(--agent-alert-bg); color: var(--agent-alert); }
754
+ .agent-supervisor-fix { display: block; margin-top: var(--agent-space-1); font: var(--agent-font-sm) ui-monospace, monospace; white-space: pre-wrap; word-break: break-all; color: var(--agent-text-dim); }
755
+ .agent-supervisor-badge.note { border-color: var(--agent-border-strong); background: transparent; color: var(--agent-text-dim); }
756
+
747
757
  .agent-failure-card { border-color: #6d3a52; background: #2a1c26; }
748
758
  .agent-failure-code {
749
759
  color: var(--agent-alert);
@@ -1097,6 +1107,16 @@
1097
1107
 
1098
1108
  .agent-model-select option:disabled { color: var(--agent-text-dim); }
1099
1109
 
1110
+ /* The selected model's derived roles (#717): one muted, read-only line. */
1111
+ .agent-model-roles {
1112
+ margin: var(--agent-space-1) 0 0;
1113
+ overflow: hidden;
1114
+ white-space: nowrap;
1115
+ text-overflow: ellipsis;
1116
+ color: var(--agent-text-dim);
1117
+ font-size: var(--agent-font-sm);
1118
+ }
1119
+
1100
1120
  .agent-composer-picks { margin-bottom: var(--agent-space-2); }
1101
1121
  .agent-effort-select { flex: 0 0 auto; width: auto; max-width: 42%; }
1102
1122
  .agent-composer-spacer { flex: 1; }
@@ -1171,3 +1191,18 @@
1171
1191
  .agent-resize { display: none; }
1172
1192
  }
1173
1193
  }
1194
+
1195
+ /* The supervisor switch beside the model picks (#728). */
1196
+ .agent-supervisor-toggle {
1197
+ flex: 0 0 auto;
1198
+ font: inherit;
1199
+ font-size: 12px;
1200
+ padding: 4px 8px;
1201
+ border-radius: 6px;
1202
+ border: 1px solid var(--agent-border, #444);
1203
+ background: transparent;
1204
+ color: var(--agent-text-dim, #999);
1205
+ cursor: pointer;
1206
+ }
1207
+ .agent-supervisor-toggle.is-on { color: var(--agent-text, #ddd); border-color: var(--agent-border-focus, #6a9); }
1208
+ .agent-supervisor-toggle:focus-visible { outline: 2px solid var(--agent-border-focus, #6a9); outline-offset: 1px; }
@@ -17,7 +17,7 @@ export const ATTACHMENT_MAX_BYTES = 4 * 1024 * 1024;
17
17
  export const ATTACHMENT_JPEG_QUALITY = 0.85;
18
18
  /** The turn envelope accepts exactly these three, so the composer refuses the
19
19
  * rest here rather than letting the sidecar reject a turn the author sent. */
20
- export const ATTACHMENT_TYPES = Object.freeze(["image/png", "image/jpeg", "image/webp"]);
20
+ const ATTACHMENT_TYPES = Object.freeze(["image/png", "image/jpeg", "image/webp"]);
21
21
  export const ATTACHMENT_LIMIT_NOTICE = "Up to 4 images per message";
22
22
  export const ATTACHMENT_TYPE_NOTICE = "Attach a PNG, JPEG or WebP image";
23
23
  export const ATTACHMENT_FAILED_NOTICE = "That image could not be attached";
@@ -49,10 +49,10 @@ export function createCanvasCommands({ store, makeNode, nodeSchemas }) {
49
49
  const handlers = {
50
50
  get_graph: () => { const graph = clone(current()); return { ...graph, nodes: graph.nodes.map((node) => ({ id: node.id, type: node.type, model: node.data?.model || null, data: node.data, position: node.position })), outputs: Object.fromEntries(graph.nodes.map((node) => [node.id, node.data?.outputs || []])) }; },
51
51
  add_node: ({ type, model, data = {}, position } = {}) => { const next = mutate((graph) => {
52
- if (typeof type !== "string" || !type || !["text", "image", "video", "audio", "api", "video-combiner", "scene", "upload", "concat", "motion-input", "shot-prompt"].includes(type)) throw new Error("Unknown node type");
52
+ if (!["text", "image", "video", "audio", "api", "video-combiner", "scene", "upload", "concat", "motion-input", "shot-prompt"].includes(type)) throw new Error("Unknown node type");
53
53
  if (position && (!Number.isFinite(position.x) || !Number.isFinite(position.y))) throw new Error("position must contain finite x and y");
54
54
  const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
55
- const modelValue = typeof model === "string" ? model : model;
55
+ const modelValue = model;
56
56
  const node = makeNode(type, id, position || nextFreePosition(graph), modelValue || null, nodeSchemas);
57
57
  if (modelValue && typeof modelValue === "string") {
58
58
  const models = nodeSchemas?.categories?.[schemaCategoryForType(type)]?.models || {};
@@ -71,5 +71,5 @@ export function createCanvasCommands({ store, makeNode, nodeSchemas }) {
71
71
  undo: () => { const previous = history.pop(); if (!previous) return { undone: false }; publish(previous); return { undone: true, graph: clone(previous) }; },
72
72
  history,
73
73
  };
74
- return { ...handlers, handlers, undo: handlers.undo, history };
74
+ return { ...handlers, handlers };
75
75
  }
@@ -8,14 +8,14 @@
8
8
  */
9
9
 
10
10
  export const COZY_SCENE_NODE_TYPE = "cozyclay-scene";
11
- export const COZY_SCENE_NODE_VERSION = 1;
11
+ const COZY_SCENE_NODE_VERSION = 1;
12
12
 
13
13
  export const COZY_SCENE_INPUTS = Object.freeze([
14
14
  { id: "asset", label: "Asset", kind: "asset" },
15
15
  { id: "motion", label: "Motion", kind: "motion", legacy: true },
16
16
  ]);
17
17
 
18
- export const COZY_SCENE_CHARACTER_HANDLE_PREFIX = "character:";
18
+ const COZY_SCENE_CHARACTER_HANDLE_PREFIX = "character:";
19
19
 
20
20
  /** Return the stable Scene target handle for a cast member. */
21
21
  export function sceneCharacterHandle(characterId) {
@@ -46,7 +46,6 @@ export function sceneInputSpecs(characters = [], { includeLegacyMotion = true }
46
46
  return specs;
47
47
  }
48
48
 
49
- /** Build the Scene input state represented by ReactFlow edges. */
50
49
  /** Restrict Scene inputs to values the Scene can actually consume. */
51
50
  export function sceneConnectionAllowed(source, targetHandle, sourceData = {}) {
52
51
  const type = typeof source === "string" ? source : source?.type;
@@ -56,6 +55,7 @@ export function sceneConnectionAllowed(source, targetHandle, sourceData = {}) {
56
55
  return false;
57
56
  }
58
57
 
58
+ /** Build the Scene input state represented by ReactFlow edges. */
59
59
  export function sceneInputsFromEdges(edges, sceneNodeId) {
60
60
  const assetInputs = [];
61
61
  const motionInputs = [];
@@ -16,13 +16,13 @@ export function executeLocalWorkflowGraph(graph, { runId = `local-${Date.now()}`
16
16
  const updates = new Map();
17
17
  const order = [];
18
18
  while (remaining.size) {
19
- const ready = [...remaining].filter((id) => (incoming.get(id) || []).every((edge) => !remaining.has(edge.source)));
19
+ const ready = [...remaining].filter((id) => incoming.get(id).every((edge) => !remaining.has(edge.source)));
20
20
  const batch = ready.length ? ready : [...remaining];
21
21
  for (const id of batch) { remaining.delete(id); order.push(id); }
22
22
  }
23
23
  for (const id of order) {
24
24
  const node = nodeById.get(id);
25
- const inputs = (incoming.get(id) || []).map((edge) => values.get(edge.source)).filter((value) => value !== undefined);
25
+ const inputs = incoming.get(id).map((edge) => values.get(edge.source)).filter((value) => value !== undefined);
26
26
  const output = localNodeOutput(node, inputs);
27
27
  values.set(id, output);
28
28
  const run = { id: runId, status: "complete", result: { outputs: [{ value: output }] } };
@@ -1,7 +1,7 @@
1
1
  import { SCENES_STORAGE_KEY, readSceneDocument } from "../scenes.js";
2
2
 
3
- export const MOTION_INPUT_NODE_TYPE = "motion-input";
4
- export const MOTION_INPUT_NODE_VERSION = 1;
3
+ const MOTION_INPUT_NODE_TYPE = "motion-input";
4
+ const MOTION_INPUT_NODE_VERSION = 1;
5
5
 
6
6
  function text(value, fallback = "") {
7
7
  return typeof value === "string" ? value : fallback;
@@ -69,7 +69,7 @@ export function characterMotionPatch(characterId, input = {}) {
69
69
  return {
70
70
  characterId: data.characterId || null,
71
71
  motionRef: {
72
- ...(data.motionRef && typeof data.motionRef === "object" ? clone(data.motionRef) : {}),
72
+ ...(data.motionRef ? clone(data.motionRef) : {}),
73
73
  url: data.url || data.objectUrl || null,
74
74
  fileName: data.fileName || null,
75
75
  mimeType: data.mimeType,
@@ -38,7 +38,7 @@ export function schemaModelEntries(nodeSchemas, category) {
38
38
  return Object.entries(models).map(([id, model]) => ({ ...model, id, name: model.name || id.replace(/-/g, " ") }));
39
39
  }
40
40
 
41
- export function schemaForModel(nodeSchemas, category, modelId) {
41
+ function schemaForModel(nodeSchemas, category, modelId) {
42
42
  const model = nodeSchemas?.categories?.[category]?.models?.[modelId];
43
43
  const input = model?.input_schema || {};
44
44
  return input?.schemas?.input_data || input;
@@ -34,13 +34,6 @@ function objectHasAsset(object, assetId) {
34
34
  return [object.assetId, object.sourceAssetId, object.matteAssetId].includes(assetId);
35
35
  }
36
36
 
37
- /** Return the document's active scene without exposing a mutable reference. */
38
- export function activeSceneRecord(document) {
39
- const scenes = Array.isArray(document?.scenes) ? document.scenes : [];
40
- const index = activeSceneIndex(scenes, document?.activeSceneId);
41
- return index < 0 ? null : scenes[index] ?? null;
42
- }
43
-
44
37
  /**
45
38
  * Add an imported image as a cutout in the active scene.
46
39
  *
@@ -116,7 +109,7 @@ export function readStoredSceneDocument(storage = globalThis.localStorage) {
116
109
  }
117
110
 
118
111
  /** Persist a scene envelope and notify both local and remote Studio tabs. */
119
- export function publishSceneDocument(document, {
112
+ function publishSceneDocument(document, {
120
113
  storage = globalThis.localStorage,
121
114
  target = globalThis,
122
115
  } = {}) {
@@ -204,7 +197,7 @@ export function subscribeToScenePlayback(onCommand, {
204
197
  }
205
198
 
206
199
  /** Import bytes through the canonical asset pipeline and warm the texture cache. */
207
- export async function importWorkflowImage(file, {
200
+ async function importWorkflowImage(file, {
208
201
  importer = importImageFile,
209
202
  remember = rememberAsset,
210
203
  importOptions,
@@ -3,7 +3,7 @@
3
3
  // the unit test share one definition of what a Shot Prompt node produces.
4
4
  import { buildShotPrompt } from "../shot-prompt.js";
5
5
 
6
- export const SHOT_PROMPT_TARGETS = Object.freeze(["image", "video"]);
6
+ const SHOT_PROMPT_TARGETS = Object.freeze(["image", "video"]);
7
7
 
8
8
  /** Defaults a fresh Shot Prompt node starts with. */
9
9
  export function shotPromptNodeData() {
@@ -1,6 +1,6 @@
1
1
  // Fal H3 Max Turbo I2V schema: https://fal.ai/models/minimax/h3-max-turbo/image-to-video/api
2
2
  export const DEFAULT_FAL_VIDEO_MODEL = "minimax/h3-max-turbo/image-to-video";
3
- export const COMFY_VIDEO_ASPECTS = Object.freeze(["16:9", "9:16", "1:1", "21:9", "12:7"]);
3
+ const COMFY_VIDEO_ASPECTS = Object.freeze(["16:9", "9:16", "1:1", "21:9", "12:7"]);
4
4
 
5
5
  export function falVideoContract(model = DEFAULT_FAL_VIDEO_MODEL) {
6
6
  if (model === DEFAULT_FAL_VIDEO_MODEL) return { name: "H3 Max Turbo", aspectFromImage: true, cameraLocked: true, aspects: [], minDuration: 5, maxDuration: 15, integerDuration: true, defaultDuration: 5, resolution: ["480P", "768P", "1080P"], defaultResolution: "480P" };
@@ -52,8 +52,6 @@ export function outputKind(value) {
52
52
  return value.trim() ? "other" : null;
53
53
  }
54
54
 
55
- /* ------------------------------------------------------------- paths ---- */
56
-
57
55
  function outputPaths(data) {
58
56
  const paths = [["resultUrl"], ["videoUrl"], ["lastOutput", "renderUrl"], ["lastOutput", "sceneUrl"], ["fileUrl"]];
59
57
  if (Array.isArray(data.versions)) data.versions.forEach((_, index) => paths.push(["versions", index, "dataUrl"]));
@@ -106,8 +104,6 @@ async function mapOutputs(graph, visit) {
106
104
  return { ...graph, nodes };
107
105
  }
108
106
 
109
- /* -------------------------------------------------------------- refs ---- */
110
-
111
107
  /**
112
108
  * Every output slot that holds something, with where it lives and what it is.
113
109
  * `field` reads like a path (`versions[2].dataUrl`) so a manifest row can point
@@ -126,8 +122,6 @@ export function workflowOutputRefs(graph) {
126
122
  return refs;
127
123
  }
128
124
 
129
- /* ----------------------------------------------------------- data URLs -- */
130
-
131
125
  function bytesToBase64(bytes) {
132
126
  let binary = "";
133
127
  for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
@@ -196,8 +190,6 @@ export function imageDimensions(bytes) {
196
190
  return null;
197
191
  }
198
192
 
199
- /* --------------------------------------------------------- intern/resolve */
200
-
201
193
  /**
202
194
  * Save path. Every `data:image/*` output becomes `{ assetRef }` and the bytes
203
195
  * come back once per distinct picture as an asset record in the shape the